@kikimoradev/kcloud 1.1.0 → 1.2.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 +531 -174
  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,18 @@ 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
847
  "volume.invalid_size": "rule_violation",
835
- "notification.not_found": "not_found"
848
+ "notification.not_found": "not_found",
849
+ "logo.not_found": "not_found",
850
+ "logo.unreadable": "rule_violation",
851
+ "logo.too_small": "rule_violation",
852
+ "logo.too_large": "rule_violation",
853
+ "logo.type_mismatch": "rule_violation"
836
854
  });
837
855
  const errorCodeSchema = z.enum(errorCodes);
838
856
  const errorDetailsSchema = z.record(z.string(), z.union([
@@ -1215,11 +1233,6 @@ z.object({
1215
1233
  sidecarsRunning: z.int().nonnegative(),
1216
1234
  volumeBytes: z.int().nonnegative().nullable()
1217
1235
  }),
1218
- sessionLogs: z.object({
1219
- prunedDirectories: z.int().nonnegative(),
1220
- lastPrunedAt: z.iso.datetime().nullable(),
1221
- failures: z.int().nonnegative()
1222
- }),
1223
1236
  metrics: runtimeMetricsSchema,
1224
1237
  deadLetters: z.array(deadLetterQueueSchema),
1225
1238
  degradedAgents: z.array(degradedAgentSchema)
@@ -1442,17 +1455,21 @@ const memorySummarySchema = z.object({
1442
1455
  sessionId: z.string().nullable(),
1443
1456
  createdAt: z.iso.datetime()
1444
1457
  }).meta({ id: "MemorySummary" });
1445
- z.object({
1446
- items: z.array(memorySummarySchema),
1458
+ const memoryOutcomes = ["succeeded", "failed"];
1459
+ const memoryOutcomeSchema = z.enum(memoryOutcomes);
1460
+ pageSchema(memorySummarySchema).extend({
1447
1461
  source: z.enum(["worker", "unavailable"]),
1448
1462
  staleReason: z.enum([
1449
1463
  "unreachable",
1450
1464
  "timeout",
1451
1465
  "stopped"
1452
- ]).nullable()
1453
- }).meta({ id: "AgentMemoryList" });
1466
+ ]).nullable(),
1467
+ truncated: z.boolean()
1468
+ }).meta({ id: "AgentMemoryPage" });
1454
1469
  z.object({
1455
1470
  q: z.string().max(200).optional(),
1471
+ outcome: memoryOutcomeSchema.optional(),
1472
+ cursor: cursorSchema.optional(),
1456
1473
  limit: z.coerce.number().int().min(1).max(100).default(20)
1457
1474
  });
1458
1475
  const costPointSchema = z.object({
@@ -1565,7 +1582,7 @@ z.object({
1565
1582
  organizationId: idSchema,
1566
1583
  accountId: idSchema
1567
1584
  });
1568
- z.enum(["monitor", "executor"]);
1585
+ const sessionRoleSchema = z.enum(["monitor", "executor"]);
1569
1586
  const sessionSlotCountSchema = z.int().min(1).max(64);
1570
1587
  const sessionQueueEntrySchema = z.object({
1571
1588
  agentId: idSchema,
@@ -1574,8 +1591,21 @@ const sessionQueueEntrySchema = z.object({
1574
1591
  projectName: z.string().nullable(),
1575
1592
  position: z.int().positive(),
1576
1593
  since: z.iso.datetime(),
1577
- taskId: z.string().nullable()
1594
+ taskId: z.string().nullable(),
1595
+ taskTitle: z.string().nullable(),
1596
+ readable: z.boolean()
1578
1597
  }).meta({ id: "SessionQueueEntry" });
1598
+ const sessionHolderSchema = z.object({
1599
+ agentId: idSchema,
1600
+ agentName: z.string().nullable(),
1601
+ projectId: idSchema.nullable(),
1602
+ projectName: z.string().nullable(),
1603
+ since: z.iso.datetime().nullable(),
1604
+ taskId: z.string().nullable(),
1605
+ taskTitle: z.string().nullable(),
1606
+ readable: z.boolean(),
1607
+ pid: z.int().positive().nullable()
1608
+ }).meta({ id: "SessionHolder" });
1579
1609
  const sessionPoolSchema = z.object({
1580
1610
  slots: sessionSlotCountSchema.nullable(),
1581
1611
  effective: z.int().nonnegative(),
@@ -1602,6 +1632,12 @@ const agentAdmissionSchema = z.object({
1602
1632
  monitor: agentAdmissionStateSchema.nullable(),
1603
1633
  executor: agentAdmissionStateSchema.nullable()
1604
1634
  }).meta({ id: "AgentAdmission" });
1635
+ const admissionHoldSchema = z.object({
1636
+ pools: z.array(sessionRoleSchema),
1637
+ sessions: z.int().nonnegative(),
1638
+ target: z.string().nullable(),
1639
+ updating: z.boolean()
1640
+ }).meta({ id: "AdmissionHold" });
1605
1641
  const nodeStateSchema = z.enum([
1606
1642
  "pending",
1607
1643
  "online",
@@ -1676,6 +1712,7 @@ const nodePoolSchema = z.object({
1676
1712
  slots: z.int().nonnegative(),
1677
1713
  inUse: z.int().nonnegative(),
1678
1714
  waiting: z.array(sessionQueueEntrySchema),
1715
+ holders: z.array(sessionHolderSchema).nullable(),
1679
1716
  usage: poolUsageSchema.nullable()
1680
1717
  }).meta({ id: "NodePool" });
1681
1718
  const nodePoolsSchema = z.object({
@@ -1761,6 +1798,7 @@ const nodeUpdateSchema = z.object({
1761
1798
  target: z.string().nullable(),
1762
1799
  sessions: z.int().nonnegative(),
1763
1800
  since: z.iso.datetime().nullable(),
1801
+ held: z.array(sessionRoleSchema),
1764
1802
  last: z.object({
1765
1803
  outcome: z.enum([
1766
1804
  "updated",
@@ -1895,16 +1933,26 @@ const splitFits = (split, host) => {
1895
1933
  return overflow.cpus <= 0 && overflow.memoryMb <= 0;
1896
1934
  };
1897
1935
  //#endregion
1936
+ //#region ../../packages/contracts/src/appearance.ts
1937
+ const colorSchema = z.string().regex(hexColorPattern);
1938
+ const logoMaxBytes = 524288;
1939
+ const logoHashSchema = z.string().regex(logoHashPattern);
1940
+ z.instanceof(Uint8Array).meta({ description: "The bytes of a PNG, JPEG or WebP image." });
1941
+ z.instanceof(Uint8Array).meta({ description: "The bytes of the WebP image." });
1942
+ z.object({ v: logoHashSchema.optional() });
1943
+ //#endregion
1898
1944
  //#region ../../packages/contracts/src/identity.ts
1899
1945
  const emailSchema = z.email().max(254);
1900
1946
  const passwordSchema = z.string().min(12).max(128);
1901
1947
  const userStatusSchema = z.enum(["active", "disabled"]);
1902
1948
  const organizationNameSchema = z.string().trim().min(2).max(80);
1903
- const organizationSlugSchema = z.string().regex(slugPattern);
1949
+ const organizationSlugSchema = z.string().regex(slugPattern).refine((slug) => !uuidShapePattern.test(slug));
1904
1950
  const organizationSchema = z.object({
1905
1951
  id: idSchema,
1906
1952
  name: z.string(),
1907
1953
  slug: organizationSlugSchema,
1954
+ color: colorSchema,
1955
+ logoHash: logoHashSchema.nullable(),
1908
1956
  createdAt: z.iso.datetime()
1909
1957
  }).meta({ id: "Organization" });
1910
1958
  z.object({ items: z.array(organizationSchema) }).meta({ id: "OrganizationList" });
@@ -1962,6 +2010,11 @@ z.object({
1962
2010
  initialPassword: passwordSchema.optional()
1963
2011
  }).optional()
1964
2012
  }).meta({ id: "CreateOrganizationBody" });
2013
+ const updateOrganizationBodySchema = z.object({
2014
+ name: organizationNameSchema,
2015
+ slug: organizationSlugSchema,
2016
+ color: colorSchema
2017
+ }).meta({ id: "UpdateOrganizationBody" });
1965
2018
  z.object({ temporaryPassword: z.string() }).meta({ id: "TemporaryPassword" });
1966
2019
  z.object({ items: z.array(userSchema) }).meta({ id: "UserList" });
1967
2020
  z.object({ id: idSchema });
@@ -2030,6 +2083,9 @@ const auditActionSchema = z.enum([
2030
2083
  "platform.bootstrapped",
2031
2084
  "organization.created",
2032
2085
  "organization.renamed",
2086
+ "organization.updated",
2087
+ "organization.logo_set",
2088
+ "organization.logo_removed",
2033
2089
  "user.created",
2034
2090
  "user.role_changed",
2035
2091
  "user.disabled",
@@ -2043,6 +2099,8 @@ const auditActionSchema = z.enum([
2043
2099
  "token.revoked",
2044
2100
  "project.created",
2045
2101
  "project.updated",
2102
+ "project.logo_set",
2103
+ "project.logo_removed",
2046
2104
  "project.member_added",
2047
2105
  "project.member_role_changed",
2048
2106
  "project.member_removed",
@@ -2151,7 +2209,6 @@ pageSchema(auditEntrySchema).meta({ id: "AuditPage" });
2151
2209
  //#endregion
2152
2210
  //#region ../../packages/contracts/src/projects.ts
2153
2211
  const slugSchema = z.string().regex(slugPattern);
2154
- const colorSchema = z.string().regex(hexColorPattern);
2155
2212
  const projectSchema = z.object({
2156
2213
  id: idSchema,
2157
2214
  organizationId: idSchema,
@@ -2159,6 +2216,7 @@ const projectSchema = z.object({
2159
2216
  slug: slugSchema,
2160
2217
  description: z.string(),
2161
2218
  color: colorSchema,
2219
+ logoHash: logoHashSchema.nullable(),
2162
2220
  dockerAllowed: z.boolean(),
2163
2221
  memberCount: z.int().nonnegative(),
2164
2222
  createdAt: z.iso.datetime(),
@@ -2587,6 +2645,7 @@ const agentRuntimeSchema = z.object({
2587
2645
  stopMode: agentStopModeSchema.nullable(),
2588
2646
  stopDeadlineAt: z.iso.datetime().nullable(),
2589
2647
  admission: agentAdmissionSchema.nullable(),
2648
+ admissionHold: admissionHoldSchema.nullable(),
2590
2649
  poolLimit: poolLimitSchema.nullable()
2591
2650
  }).meta({ id: "AgentRuntime" });
2592
2651
  const agentListItemSchema = z.object({
@@ -2935,19 +2994,6 @@ const nodeVolumesSchema = z.discriminatedUnion("source", [z.object({
2935
2994
  source: z.literal("ok"),
2936
2995
  items: z.array(nodeVolumeSchema)
2937
2996
  }), 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
2997
  z.object({
2952
2998
  droplet: nodeDropletSchema,
2953
2999
  volumes: nodeVolumesSchema,
@@ -2956,23 +3002,16 @@ z.object({
2956
3002
  data: storageReadingSchema,
2957
3003
  agents: storageReadingSchema
2958
3004
  }),
2959
- sizes: nodeSizesSchema,
2960
3005
  platform: z.object({
2961
3006
  version: z.string(),
2962
3007
  instance: z.string(),
2963
3008
  scriptsVersion: z.string(),
2964
3009
  dockerVersion: z.string().nullable(),
2965
3010
  dockerRootDir: z.string().nullable()
2966
- }),
2967
- maintenance: z.object({
2968
- quiescedAt: instantSchema$1.nullable(),
2969
- pending: z.array(idSchema)
2970
- }),
2971
- capacity: capacityForecastSchema
3011
+ })
2972
3012
  }).meta({ id: "PlatformNode" });
2973
3013
  z.object({ role: volumeRoleSchema });
2974
3014
  z.object({ sizeGb: z.int().positive() }).meta({ id: "ResizeVolumeBody" });
2975
- z.object({ agents: z.int().nonnegative() }).meta({ id: "QuiesceResult" });
2976
3015
  const requesterSchema = z.object({
2977
3016
  userId: idSchema,
2978
3017
  label: z.string().nullable()
@@ -3076,22 +3115,17 @@ const registryUnavailableSchema = z.object({
3076
3115
  detail: z.string().nullable()
3077
3116
  }).meta({ id: "RegistryUnavailable" });
3078
3117
  const hostOperationsAvailabilities = ["enabled", "disabled"];
3079
- const backupsAvailabilities = ["enabled", "unavailable"];
3080
3118
  const installedImageSchema = z.string().regex(pinnedPlatformImagePattern).nullable();
3081
3119
  z.object({
3082
3120
  version: z.string(),
3121
+ environment: z.string().nullable(),
3083
3122
  apiImage: installedImageSchema,
3084
3123
  webImage: installedImageSchema,
3085
3124
  nodeImage: installedImageSchema,
3086
3125
  latest: z.string().nullable(),
3087
3126
  latestError: registryUnavailableSchema.nullable(),
3088
- versions: z.array(z.string()),
3089
- upgradeable: z.boolean(),
3090
- scriptsVersion: z.string(),
3091
- expectedScriptsVersion: z.string(),
3092
3127
  imagePrefix: z.string(),
3093
3128
  hostOperations: z.enum(hostOperationsAvailabilities),
3094
- backups: z.enum(backupsAvailabilities),
3095
3129
  operations: z.array(hostOperationSchema)
3096
3130
  }).meta({ id: "Platform" });
3097
3131
  const keyVersionSchema = z.int().positive();
@@ -3114,7 +3148,6 @@ z.object({
3114
3148
  versions: z.array(masterKeyVersionSchema),
3115
3149
  rotationPending: z.boolean()
3116
3150
  }).meta({ id: "MasterKey" });
3117
- z.object({ version: z.string().regex(versionTagPattern) }).meta({ id: "UpgradePlatformBody" });
3118
3151
  const metricsRangeSchema = z.enum([
3119
3152
  "1h",
3120
3153
  "24h",
@@ -3195,24 +3228,6 @@ z.object({
3195
3228
  }).meta({ id: "AgentMetrics" });
3196
3229
  z.object({ range: metricsRangeSchema });
3197
3230
  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
3231
  const notificationKindSchema = z.enum([
3217
3232
  "credential.verification_failed",
3218
3233
  "credential.expiring",
@@ -3368,6 +3383,7 @@ const detailsOf$1 = (body) => {
3368
3383
  const parsed = errorBodySchema.safeParse(body);
3369
3384
  return parsed.success ? parsed.data.details ?? {} : {};
3370
3385
  };
3386
+ const hintText = (hint, details) => typeof hint === "string" ? hint : hint(details);
3371
3387
  //#endregion
3372
3388
  //#region src/client/api.ts
3373
3389
  const defaultTimeoutMs = 3e4;
@@ -3424,7 +3440,7 @@ const createApi = ({ origin, token, fetch = globalThis.fetch, timeoutMs = defaul
3424
3440
  const refused = (action, { error, response }, hints) => {
3425
3441
  const failure = new ApiError(response.status, codeOf(error), action, detailsOf$1(error));
3426
3442
  const hint = failure.code === null ? void 0 : hints[failure.code];
3427
- return hint === void 0 ? failure : new CliError(failure.message, hint, { cause: failure });
3443
+ return hint === void 0 ? failure : new CliError(failure.message, hintText(hint, failure.details), { cause: failure });
3428
3444
  };
3429
3445
  const dataOf = (action, answer, hints = {}) => {
3430
3446
  if (answer.data === void 0) throw refused(action, answer, hints);
@@ -3796,7 +3812,7 @@ const createLoginCommand = (adaptersOf) => defineCommand({
3796
3812
  }
3797
3813
  });
3798
3814
  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.` });
3815
+ 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
3816
  prompt.note([
3801
3817
  `Code ${started.userCode}`,
3802
3818
  `Address ${started.verificationUri}`,
@@ -4243,10 +4259,15 @@ const runOf = async (adaptersOf, { settings, context }) => {
4243
4259
  //#region src/commands/agents/shared.ts
4244
4260
  const agentOption = { project: { type: "string" } };
4245
4261
  const agentUsage = [["--project <name>", "the project of the agent, when the agent is named alone"]];
4262
+ const actionsAfterPause = /* @__PURE__ */ new Set([
4263
+ "update",
4264
+ "restart",
4265
+ "resume"
4266
+ ]);
4246
4267
  const agentHints = {
4247
4268
  "agent.not_found": "kcloud agents list --project <name> shows the agents.",
4248
4269
  "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.",
4270
+ "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
4271
  "agent.busy": "The agent is working on another action; try again in a moment.",
4251
4272
  "concurrency.conflict": "Someone changed the agent meanwhile; read it again and retry.",
4252
4273
  "agent.node_unsuitable": "The node lacks a capability the agent requires; kcloud nodes show <node> lists what it offers.",
@@ -4474,12 +4495,39 @@ const enforcementText = ({ state, reason }) => {
4474
4495
  case "unsupported": return `The node holds no pool to a limit: ${reason === null ? "it cannot" : unsupportedReasons[reason] ?? reason}.`;
4475
4496
  }
4476
4497
  };
4477
- const memoryOf = (pool) => {
4498
+ const memoryOf$1 = (pool) => {
4478
4499
  if (pool.usage === null) return "-";
4479
4500
  const { memoryBytes, memoryMaxBytes } = pool.usage;
4480
4501
  return memoryMaxBytes === null ? bytesOf(memoryBytes) : `${bytesOf(memoryBytes)} of ${bytesOf(memoryMaxBytes)}`;
4481
4502
  };
4482
4503
  const roles = ["monitor", "executor"];
4504
+ const workOf = (role, { taskId, taskTitle, readable }) => {
4505
+ if (readable === false) return "another project";
4506
+ if (taskId === null) return role === "monitor" ? "monitor cycle" : "-";
4507
+ return taskTitle == null ? taskId : `${taskId} ${taskTitle}`;
4508
+ };
4509
+ const holdersOf = (pools) => {
4510
+ if (roles.every((role) => pools[role].holders == null)) return null;
4511
+ const held = roles.flatMap((role) => (pools[role].holders ?? []).map((holder) => [
4512
+ role,
4513
+ holder.projectName ?? "-",
4514
+ holder.agentName ?? holder.agentId,
4515
+ workOf(role, holder),
4516
+ holder.since ?? "-",
4517
+ holder.pid == null ? "-" : String(holder.pid)
4518
+ ]));
4519
+ return held.length === 0 ? "No session holds a slot." : renderTable([
4520
+ { header: "HOLDS" },
4521
+ { header: "PROJECT" },
4522
+ { header: "AGENT" },
4523
+ { header: "WORK" },
4524
+ { header: "SINCE" },
4525
+ {
4526
+ header: "PID",
4527
+ align: "right"
4528
+ }
4529
+ ], held);
4530
+ };
4483
4531
  const queueOf = (pools) => {
4484
4532
  const waiting = roles.flatMap((role) => pools[role].waiting.map((entry) => [
4485
4533
  role,
@@ -4487,7 +4535,7 @@ const queueOf = (pools) => {
4487
4535
  entry.projectName ?? "-",
4488
4536
  entry.agentName ?? entry.agentId,
4489
4537
  entry.since,
4490
- entry.taskId ?? "-"
4538
+ workOf(role, entry)
4491
4539
  ]));
4492
4540
  return waiting.length === 0 ? "No session waits for a slot." : renderTable([
4493
4541
  { header: "WAITS FOR" },
@@ -4498,37 +4546,41 @@ const queueOf = (pools) => {
4498
4546
  { header: "PROJECT" },
4499
4547
  { header: "AGENT" },
4500
4548
  { header: "SINCE" },
4501
- { header: "TASK" }
4549
+ { header: "WORK" }
4502
4550
  ], waiting);
4503
4551
  };
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");
4552
+ const poolsText = (pools) => {
4553
+ const holders = holdersOf(pools);
4554
+ return [
4555
+ enforcementText(pools.enforcement),
4556
+ renderTable([
4557
+ { header: "POOL" },
4558
+ { header: "SLOTS IN USE" },
4559
+ {
4560
+ header: "CPU IN USE",
4561
+ align: "right"
4562
+ },
4563
+ { header: "MEMORY IN USE" },
4564
+ {
4565
+ header: "MEMORY KILLS",
4566
+ align: "right"
4567
+ }
4568
+ ], roles.map((role) => {
4569
+ const pool = pools[role];
4570
+ const cpus = pool.usage?.cpus ?? null;
4571
+ return [
4572
+ role,
4573
+ `${pool.inUse} of ${pool.slots}`,
4574
+ cpus === null ? "-" : cpus.toFixed(2),
4575
+ memoryOf$1(pool),
4576
+ pool.usage === null ? "-" : String(pool.usage.memoryKills)
4577
+ ];
4578
+ })),
4579
+ ...holders === null ? [] : ["", holders],
4580
+ "",
4581
+ queueOf(pools)
4582
+ ].join("\n");
4583
+ };
4532
4584
  //#endregion
4533
4585
  //#region src/commands/agents/plan.ts
4534
4586
  const agentPlanOf = (agent) => ({
@@ -4708,6 +4760,15 @@ const admissionRowsOf = (agent) => {
4708
4760
  const { admission } = agent.runtime;
4709
4761
  return admission === null ? [] : [["monitor slot", slotOf(admission.monitor)], ["executor slot", slotOf(admission.executor)]];
4710
4762
  };
4763
+ const admissionHoldRowsOf = ({ runtime }) => {
4764
+ const hold = runtime.admissionHold;
4765
+ if (hold == null) return [];
4766
+ if (hold.updating) return [["admissions", `held while the node updates to ${hold.target ?? "-"}`]];
4767
+ const all = hold.pools.includes("monitor");
4768
+ const noun = all ? "session" : "executor session";
4769
+ const waits = `${String(hold.sessions)} ${noun}${hold.sessions === 1 ? "" : "s"}`;
4770
+ return [["admissions", `${all ? "held" : "executor held"} for the node update to ${hold.target ?? "-"}, waiting for ${waits}`]];
4771
+ };
4711
4772
  const poolLimitRowsOf = ({ runtime }) => runtime.poolLimit === null ? [] : [["pool limit", poolLimitText(runtime.poolLimit)]];
4712
4773
  const detailsOf = (agent) => [
4713
4774
  renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
@@ -4726,6 +4787,7 @@ const detailsOf = (agent) => [
4726
4787
  ["container", agent.runtime.containerId ?? "none"],
4727
4788
  ["health", agent.runtime.health],
4728
4789
  ...admissionRowsOf(agent),
4790
+ ...admissionHoldRowsOf(agent),
4729
4791
  ...poolLimitRowsOf(agent),
4730
4792
  ["pending", agent.restartRequired ? agent.restartReasons.join(", ") : "-"],
4731
4793
  ["actions", agent.availableActions.join(", ")],
@@ -5061,30 +5123,29 @@ const lifecycleCommands = (adaptersOf) => agentActions.map((action) => defineCom
5061
5123
  }));
5062
5124
  //#endregion
5063
5125
  //#region src/client/pages.ts
5064
- const windowOf = (limit, all, fallback = 50) => {
5126
+ const windowOf = (limit, all, fallback = 50, largest = 200) => {
5065
5127
  if (limit === void 0) return {
5066
5128
  limit: fallback,
5067
5129
  all
5068
5130
  };
5069
5131
  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.`);
5132
+ 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
5133
  return {
5072
5134
  limit: size,
5073
5135
  all
5074
5136
  };
5075
5137
  };
5076
- const collect = async (page, { limit, all }) => {
5077
- const first = await page(null, Math.min(limit, 200));
5138
+ const collect = async (page, { limit, all }, largest = 200) => {
5139
+ const first = await page(null, Math.min(limit, largest));
5078
5140
  if (!all) return first;
5079
5141
  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;
5142
+ let last = first;
5143
+ while (last.nextCursor !== null) {
5144
+ last = await page(last.nextCursor, largest);
5145
+ items.push(...last.items);
5085
5146
  }
5086
5147
  return {
5087
- ...first,
5148
+ ...last,
5088
5149
  items,
5089
5150
  nextCursor: null
5090
5151
  };
@@ -5131,6 +5192,43 @@ const transcriptOf = async (api, agentId, sessionId, all) => collect(async (curs
5131
5192
  limit: 200,
5132
5193
  all
5133
5194
  });
5195
+ const memoryOf = (api, agentId, { query, outcome }, window) => collect(async (cursor, limit) => dataOf("search what the agent remembers", await api.GET("/agents/{id}/memory", { params: {
5196
+ path: { id: agentId },
5197
+ query: {
5198
+ limit,
5199
+ ...query === void 0 ? {} : { q: query },
5200
+ ...outcome === void 0 ? {} : { outcome },
5201
+ ...cursor === null ? {} : { cursor }
5202
+ }
5203
+ } }), agentHints), window, 100);
5204
+ const memoryReasons = {
5205
+ stopped: "the agent is not running",
5206
+ unreachable: "the agent did not answer on its control socket",
5207
+ timeout: "the agent did not answer in time"
5208
+ };
5209
+ const unreadReasonOf = (page) => page.staleReason === null ? "the agent did not answer" : memoryReasons[page.staleReason];
5210
+ const memoryText = (page) => {
5211
+ const unread = page.source === "unavailable";
5212
+ if (unread && page.items.length === 0) return `The memory cannot be read: ${unreadReasonOf(page)}.`;
5213
+ if (page.items.length === 0) return "The agent remembers nothing that matches.";
5214
+ return [
5215
+ renderTable([
5216
+ { header: "WHEN" },
5217
+ { header: "OUTCOME" },
5218
+ { header: "TASK" },
5219
+ { header: "TITLE" },
5220
+ { header: "HEADLINE" }
5221
+ ], page.items.map((summary) => [
5222
+ summary.createdAt,
5223
+ summary.ok ? "ok" : "failed",
5224
+ summary.taskId,
5225
+ summary.title,
5226
+ summary.headline
5227
+ ])),
5228
+ ...unread ? ["", `The rest of the memory cannot be read: ${unreadReasonOf(page)}.`] : [],
5229
+ ...page.truncated ? ["", `The agent returns at most 100 summaries, so the list may miss older ones.`] : []
5230
+ ].join("\n");
5231
+ };
5134
5232
  const observabilityCommands = (adaptersOf) => [
5135
5233
  defineCommand({
5136
5234
  name: "sessions",
@@ -5206,7 +5304,9 @@ const observabilityCommands = (adaptersOf) => [
5206
5304
  const run = await runOf(adaptersOf, invocation);
5207
5305
  const agentId = await agentFrom(run, invocation.positionals[0], invocation.values.project);
5208
5306
  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"));
5307
+ const code = printed(run, page.items, (entries) => entries.length === 0 ? "The session left no transcript." : entries.map((entry) => JSON.stringify(entry)).join("\n"));
5308
+ if (page.nextCursor !== null && !run.settings.json) run.context.terminal.error("More entries follow; --all prints the whole session.");
5309
+ return code;
5210
5310
  }
5211
5311
  }),
5212
5312
  defineCommand({
@@ -5342,44 +5442,35 @@ const observabilityCommands = (adaptersOf) => [
5342
5442
  defineCommand({
5343
5443
  name: "memory",
5344
5444
  summary: "Search what an agent remembers of its tasks.",
5345
- usage: "agents memory <agent> [--project <name>] [--query <text>] [--limit <n>]",
5445
+ usage: "agents memory <agent> [--project <name>] [--query <text>] [--outcome <outcome>] [--limit <n>] [--all]",
5346
5446
  optionsUsage: [
5347
5447
  ...agentUsage,
5348
5448
  ["--query <text>", "only the summaries that match this text"],
5349
- ["--limit <n>", "how many summaries (default: 20)"]
5449
+ ["--outcome <outcome>", `only the summaries of one outcome: ${memoryOutcomes.join(", ")}`],
5450
+ ["--limit <n>", `how many summaries (default: 20)`],
5451
+ allUsage
5350
5452
  ],
5351
5453
  options: {
5352
5454
  ...agentOption,
5455
+ ...pageOptions,
5353
5456
  query: { type: "string" },
5354
- limit: { type: "string" }
5457
+ outcome: { type: "string" }
5355
5458
  },
5356
5459
  positionals: {
5357
5460
  min: 1,
5358
5461
  max: 1
5359
5462
  },
5360
5463
  run: async (invocation) => {
5464
+ const { values } = invocation;
5361
5465
  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
- ])));
5466
+ const agentId = await agentFrom(run, invocation.positionals[0], values.project);
5467
+ const outcome = memoryOutcomes.find((candidate) => candidate === values.outcome);
5468
+ 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(", ")}.`);
5469
+ const page = await memoryOf(run.api, agentId, {
5470
+ query: values.query,
5471
+ outcome
5472
+ }, windowOf(values.limit, values.all, 20, 100));
5473
+ return printed(run, page, memoryText);
5383
5474
  }
5384
5475
  })
5385
5476
  ];
@@ -5953,25 +6044,59 @@ const secretUsage = [
5953
6044
  //#region src/commands/credentials.ts
5954
6045
  const projectOption$2 = { project: { type: "string" } };
5955
6046
  const projectUsage$2 = [["--project <name>", "the project the credential belongs to, by slug, name or id"]];
6047
+ const usageUnavailable = "Only a Claude Code OAuth token reports the usage of a subscription.";
5956
6048
  const credentialHints = {
5957
6049
  "project.not_found": "kcloud projects list shows the projects you are a member of.",
5958
6050
  "credential.not_found": "kcloud credentials list --project <name> shows them.",
5959
6051
  "credential.label_taken": "Another credential of the project already has that label.",
5960
- "credential.in_use": "Point the agents at another credential first."
6052
+ "credential.in_use": "Point the agents at another credential first.",
6053
+ "credential.usage_unavailable": usageUnavailable
6054
+ };
6055
+ const percent = 100;
6056
+ const notApplicable = "-";
6057
+ const usedOf = (window) => window === null ? notApplicable : `${String(Math.round(window.utilization * percent))}%`;
6058
+ const shareOf = (window) => window?.status === "rejected" ? "limit" : usedOf(window);
6059
+ const usageSummaryOf = ({ kind, usage }) => {
6060
+ if (kind !== "claude_oauth_token") return notApplicable;
6061
+ if (usage === null) return "no data";
6062
+ const summary = `5h ${shareOf(usage.fiveHour)} 7d ${shareOf(usage.sevenDay)}`;
6063
+ return usage.stale ? `${summary} (stale)` : summary;
6064
+ };
6065
+ const usageReportOf = (credential) => {
6066
+ const { usage } = credential;
6067
+ if (usage === null) return `No usage of ${credential.label} was read yet.`;
6068
+ const windows = [["5h", usage.fiveHour], ["7d", usage.sevenDay]];
6069
+ return `${renderTable([
6070
+ { header: "WINDOW" },
6071
+ { header: "USED" },
6072
+ { header: "STATUS" },
6073
+ { header: "RESETS" }
6074
+ ], windows.map(([name, window]) => [
6075
+ name,
6076
+ usedOf(window),
6077
+ window?.status ?? notApplicable,
6078
+ window?.resetsAt ?? notApplicable
6079
+ ]))}\n${usage.stale ? `Read at ${usage.readAt}; the latest reading failed.` : `Read at ${usage.readAt}.`}`;
6080
+ };
6081
+ const isoDateLength = 10;
6082
+ const expiryOf = ({ expiresAt, expiresAtComputed }) => {
6083
+ if (expiresAt === null) return "never";
6084
+ return expiresAtComputed ? `about ${expiresAt.slice(0, isoDateLength)}` : expiresAt;
5961
6085
  };
5962
6086
  const projectFrom$2 = async (run, given) => {
5963
6087
  if (given === void 0) throw new UsageError("This command needs a project.", "Pass --project <name>; kcloud projects list shows them.");
5964
6088
  return resolveProjectId(run, given);
5965
6089
  };
5966
6090
  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) => {
6091
+ const resolveCredential = async (api, projectId, reference) => {
5968
6092
  const credentials = await credentialsOf(api, projectId);
5969
6093
  const found = credentials.filter((credential) => credential.id === reference || credential.label === reference);
5970
6094
  const [first] = found;
5971
6095
  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
6096
  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;
6097
+ return first;
5974
6098
  };
6099
+ const resolveCredentialId = async (api, projectId, reference) => (await resolveCredential(api, projectId, reference)).id;
5975
6100
  const createCredentialsCommand = (adaptersOf) => defineGroup({
5976
6101
  name: "credentials",
5977
6102
  summary: "The Claude and GitHub credentials of a project.",
@@ -5997,13 +6122,15 @@ const createCredentialsCommand = (adaptersOf) => defineGroup({
5997
6122
  { header: "FINGERPRINT" },
5998
6123
  { header: "VERIFIED" },
5999
6124
  { header: "EXPIRES" },
6125
+ { header: "USAGE" },
6000
6126
  { header: "ID" }
6001
6127
  ], credentials.map((credential) => [
6002
6128
  credential.label,
6003
6129
  credential.kind,
6004
6130
  credential.fingerprint,
6005
6131
  credential.verification,
6006
- credential.expiresAt ?? "never",
6132
+ expiryOf(credential),
6133
+ usageSummaryOf(credential),
6007
6134
  credential.id
6008
6135
  ])));
6009
6136
  }
@@ -6149,6 +6276,34 @@ const createCredentialsCommand = (adaptersOf) => defineGroup({
6149
6276
  return said(run, `The instance is verifying ${credentialId}; kcloud credentials list shows the answer.`);
6150
6277
  }
6151
6278
  }),
6279
+ defineCommand({
6280
+ name: "usage",
6281
+ summary: "Show the usage of the Claude subscription behind a Claude Code OAuth token.",
6282
+ usage: "credentials usage <credential> --project <name> [--refresh]",
6283
+ optionsUsage: [...projectUsage$2, ["--refresh", "read it from Anthropic now; within 30 seconds the last reading"]],
6284
+ options: {
6285
+ ...projectOption$2,
6286
+ refresh: {
6287
+ type: "boolean",
6288
+ default: false
6289
+ }
6290
+ },
6291
+ positionals: {
6292
+ min: 1,
6293
+ max: 1
6294
+ },
6295
+ run: async (invocation) => {
6296
+ const run = await runOf(adaptersOf, invocation);
6297
+ const projectId = await projectFrom$2(run, invocation.values.project);
6298
+ const stored = await resolveCredential(run.api, projectId, invocation.positionals[0] ?? "");
6299
+ if (stored.kind !== "claude_oauth_token") throw new CliError(`${stored.label} is a ${stored.kind}, which reports no usage.`, usageUnavailable);
6300
+ const credential = invocation.values.refresh ? dataOf("read the usage of the credential", await run.api.POST("/projects/{id}/credentials/{credentialId}/usage", { params: { path: {
6301
+ id: projectId,
6302
+ credentialId: stored.id
6303
+ } } }), credentialHints) : stored;
6304
+ return printed(run, credential, usageReportOf);
6305
+ }
6306
+ }),
6152
6307
  defineCommand({
6153
6308
  name: "delete",
6154
6309
  summary: "Delete a credential no agent uses.",
@@ -7010,19 +7165,31 @@ const credentialTools = [
7010
7165
  }),
7011
7166
  defineTool({
7012
7167
  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"],
7168
+ title: "Rename, verify or read the usage of a credential",
7169
+ 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.",
7170
+ operations: [
7171
+ "renameCredential",
7172
+ "verifyCredential",
7173
+ "refreshCredentialUsage"
7174
+ ],
7016
7175
  safety: "write",
7017
7176
  input: z.object({
7018
7177
  project: projectField,
7019
7178
  credential: credentialField$1,
7020
- action: z.enum(["rename", "verify"]),
7179
+ action: z.enum([
7180
+ "rename",
7181
+ "verify",
7182
+ "refresh_usage"
7183
+ ]),
7021
7184
  label: z.string().trim().min(1).optional().describe("The new label, for rename.")
7022
7185
  }),
7023
7186
  run: async ({ project, credential, action, label }, run) => {
7024
7187
  const projectId = await projectIdOf(run, project);
7025
7188
  const credentialId = await resolveCredentialId(run.api, projectId, credential);
7189
+ if (action === "refresh_usage") return dataOf("read the usage of the credential", await run.api.POST("/projects/{id}/credentials/{credentialId}/usage", { params: { path: {
7190
+ id: projectId,
7191
+ credentialId
7192
+ } } }), credentialHints);
7026
7193
  if (action === "verify") {
7027
7194
  doneWith("verify the credential", await run.api.POST("/projects/{id}/credentials/{credentialId}/verify", { params: { path: {
7028
7195
  id: projectId,
@@ -7363,21 +7530,23 @@ const observabilityTools = [
7363
7530
  defineTool({
7364
7531
  name: "agent_memory_search",
7365
7532
  title: "Search the memory of an agent",
7366
- description: "Search what an agent remembers of the tasks it finished.",
7533
+ description: "Search what an agent remembers of the tasks it finished, newest first, one page or every summary with all.",
7367
7534
  operations: ["searchAgentMemory"],
7368
7535
  safety: "read",
7369
7536
  input: z.object({
7370
7537
  ...agentFields,
7371
7538
  query: z.string().max(200).optional(),
7372
- limit: z.int().min(1).max(100).default(20)
7539
+ outcome: memoryOutcomeSchema.optional().describe("Only the summaries of tasks that succeeded or failed."),
7540
+ limit: z.int().min(1).max(100).default(20),
7541
+ all: pageFields.all
7373
7542
  }),
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)
7543
+ run: async ({ query, outcome, limit, all, ...reference }, run) => memoryOf(run.api, await agentIdOf(run, reference), {
7544
+ query,
7545
+ outcome
7546
+ }, {
7547
+ limit,
7548
+ all
7549
+ })
7381
7550
  }),
7382
7551
  defineTool({
7383
7552
  name: "agent_earlier_output",
@@ -7442,6 +7611,70 @@ const observabilityTools = [
7442
7611
  })
7443
7612
  ];
7444
7613
  //#endregion
7614
+ //#region src/commands/logo.ts
7615
+ const logoOptions = {
7616
+ logo: { type: "string" },
7617
+ "no-logo": {
7618
+ type: "boolean",
7619
+ default: false
7620
+ }
7621
+ };
7622
+ 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"]];
7623
+ const logoHints = {
7624
+ "logo.unreadable": "The file has to be a PNG, JPEG or WebP image.",
7625
+ "logo.too_small": "The image needs at least 64 pixels on each side.",
7626
+ "logo.too_large": "The image may have at most 2048 pixels on a side."
7627
+ };
7628
+ const startsWith = (bytes, prefix, at = 0) => prefix.every((byte, index) => bytes[at + index] === byte);
7629
+ const ascii = (text) => Array.from(text, (letter) => letter.charCodeAt(0));
7630
+ const signatures = [
7631
+ {
7632
+ contentType: "image/png",
7633
+ matches: (bytes) => startsWith(bytes, [
7634
+ 137,
7635
+ ...ascii("PNG\r\n"),
7636
+ 26,
7637
+ 10
7638
+ ])
7639
+ },
7640
+ {
7641
+ contentType: "image/jpeg",
7642
+ matches: (bytes) => startsWith(bytes, [
7643
+ 255,
7644
+ 216,
7645
+ 255
7646
+ ])
7647
+ },
7648
+ {
7649
+ contentType: "image/webp",
7650
+ matches: (bytes) => startsWith(bytes, ascii("RIFF")) && startsWith(bytes, ascii("WEBP"), 8)
7651
+ }
7652
+ ];
7653
+ const isLogoChange = (change) => change.logo !== void 0 || change["no-logo"] === true;
7654
+ const logoFileOf = async (workingDirectory, file) => {
7655
+ const path = resolve(workingDirectory, file);
7656
+ const bytes = await readFile(path).catch(() => {
7657
+ throw new CliError(`${path} could not be read.`);
7658
+ });
7659
+ 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.`);
7660
+ const contentType = signatures.find(({ matches }) => matches(bytes))?.contentType;
7661
+ if (contentType === void 0) throw new UsageError(`${file} is not a PNG, JPEG or WebP image.`);
7662
+ return {
7663
+ bytes,
7664
+ contentType
7665
+ };
7666
+ };
7667
+ const logoActionOf = async (workingDirectory, change) => {
7668
+ if (change.logo !== void 0 && change["no-logo"] === true) throw new UsageError("--logo and --no-logo exclude each other.");
7669
+ if (change.logo !== void 0) return logoFileOf(workingDirectory, change.logo);
7670
+ return change["no-logo"] === true ? "remove" : null;
7671
+ };
7672
+ const logoUpload = ({ bytes, contentType }) => ({
7673
+ body: bytes,
7674
+ bodySerializer: (body) => body,
7675
+ headers: { "content-type": contentType }
7676
+ });
7677
+ //#endregion
7445
7678
  //#region src/commands/roles.ts
7446
7679
  const roleOption = { role: { type: "string" } };
7447
7680
  const roleUsage = ["--role <role>", "admin or member"];
@@ -7751,6 +7984,48 @@ const costsText = (costs, { from, to }, slugs) => [
7751
7984
  "",
7752
7985
  `${costs.totalCostUsd.toFixed(2)} over ${costs.totalRuns} runs and ${costs.sessions} sessions.`
7753
7986
  ].join("\n");
7987
+ const organizationChangeHints = {
7988
+ ...organizationHints,
7989
+ "organization.slug_taken": "Another organization already uses that slug.",
7990
+ "organization.invalid_slug": "A slug holds lowercase letters, digits and hyphens.",
7991
+ "auth.forbidden": "Only an admin of the organization changes it.",
7992
+ ...logoHints
7993
+ };
7994
+ const followSlug = async (homeDirectory, origin, before, after) => {
7995
+ const store = await readInstanceStore(homeDirectory);
7996
+ const stored = store.instances[origin];
7997
+ if (stored?.organization !== before.slug) return;
7998
+ await writeInstanceStore(homeDirectory, withInstance(store, origin, {
7999
+ ...stored,
8000
+ organization: after.slug
8001
+ }));
8002
+ };
8003
+ const updatedOrganization = async (api, current, fields) => {
8004
+ if ([
8005
+ fields.name,
8006
+ fields.slug,
8007
+ fields.color
8008
+ ].every((field) => field === void 0)) return current;
8009
+ const body = {
8010
+ name: fields.name ?? current.name,
8011
+ slug: fields.slug ?? current.slug,
8012
+ color: fields.color ?? current.color
8013
+ };
8014
+ validated(updateOrganizationBodySchema, body, "a change to an organization");
8015
+ return dataOf("change the organization", await api.PATCH("/organizations/{organizationId}", {
8016
+ params: { path: { organizationId: current.id } },
8017
+ body
8018
+ }), organizationChangeHints);
8019
+ };
8020
+ const withLogo = async (api, organization, logo) => {
8021
+ const params = { path: { organizationId: organization.id } };
8022
+ if (logo === null) return organization;
8023
+ if (logo === "remove") return dataOf("remove the logo of the organization", await api.DELETE("/organizations/{organizationId}/logo", { params }), organizationChangeHints);
8024
+ return dataOf("set the logo of the organization", await api.PUT("/organizations/{organizationId}/logo", {
8025
+ params,
8026
+ ...logoUpload(logo)
8027
+ }), organizationChangeHints);
8028
+ };
7754
8029
  const createOrgsCommand = (adaptersOf) => defineGroup({
7755
8030
  name: "orgs",
7756
8031
  summary: "The organizations you can see, and the one kcloud acts in.",
@@ -7835,11 +8110,52 @@ const createOrgsCommand = (adaptersOf) => defineGroup({
7835
8110
  return printed(run, organization, (shown) => renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
7836
8111
  ["name", shown.name],
7837
8112
  ["slug", shown.slug],
8113
+ ["color", shown.color],
8114
+ ["logo", shown.logoHash ?? "-"],
7838
8115
  ["created", shown.createdAt],
7839
8116
  ["id", shown.id]
7840
8117
  ]));
7841
8118
  }
7842
8119
  }),
8120
+ defineCommand({
8121
+ name: "set",
8122
+ summary: "Change the name, slug, color or logo of an organization.",
8123
+ usage: "orgs set [<organization>] [--name <name>] [--slug <slug>] [--color <#rrggbb>] [--logo <path> | --no-logo]",
8124
+ optionsUsage: [
8125
+ ["--name <name>", "the new name"],
8126
+ ["--slug <slug>", "the new slug; scripts and --org that name the old one stop finding it"],
8127
+ ["--color <#rrggbb>", "the color of the organization in the panel"],
8128
+ ...logoUsage
8129
+ ],
8130
+ options: {
8131
+ name: { type: "string" },
8132
+ slug: { type: "string" },
8133
+ color: { type: "string" },
8134
+ ...logoOptions
8135
+ },
8136
+ positionals: {
8137
+ min: 0,
8138
+ max: 1
8139
+ },
8140
+ run: async (invocation) => {
8141
+ const { values } = invocation;
8142
+ if ([
8143
+ values.name,
8144
+ values.slug,
8145
+ values.color
8146
+ ].every((value) => value === void 0) && !isLogoChange(values)) throw new UsageError("orgs set needs --name, --slug, --color, --logo or --no-logo.");
8147
+ const logo = await logoActionOf(invocation.context.workingDirectory, values);
8148
+ const run = await runOf(adaptersOf, invocation);
8149
+ const current = await organizationOrInUse(run, invocation.positionals[0]);
8150
+ const updated = await updatedOrganization(run.api, current, values);
8151
+ if (updated.slug !== current.slug) {
8152
+ await followSlug(run.context.homeDirectory, run.origin, current, updated);
8153
+ 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.`));
8154
+ }
8155
+ const organization = await withLogo(run.api, updated, logo);
8156
+ return printed(run, organization, (shown) => `Changed ${shown.name} (${shown.slug}).`);
8157
+ }
8158
+ }),
7843
8159
  defineCommand({
7844
8160
  name: "costs",
7845
8161
  summary: "Show what the organization spent, by day and by project.",
@@ -7904,6 +8220,20 @@ const organizationTools = [
7904
8220
  return dataOf("describe the organization", await run.api.GET("/organizations/{organizationId}", { params: { path: { organizationId: id } } }), organizationHints);
7905
8221
  }
7906
8222
  }),
8223
+ defineTool({
8224
+ name: "organization_update",
8225
+ title: "Change an organization",
8226
+ 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.",
8227
+ operations: ["listOrganizations", "updateOrganization"],
8228
+ safety: "write",
8229
+ idempotent: true,
8230
+ input: z.object({
8231
+ organization: z.string().trim().min(1).optional().describe("The organization, by its slug, its name or its id."),
8232
+ name: organizationNameSchema.optional(),
8233
+ color: colorSchema.optional().describe("The color in the panel, as #rrggbb.")
8234
+ }),
8235
+ run: async ({ organization, ...change }, run) => updatedOrganization(run.api, await organizationOrInUse(run, organization), change)
8236
+ }),
7907
8237
  defineTool({
7908
8238
  name: "organization_costs",
7909
8239
  title: "Show what the organization spent",
@@ -8433,19 +8763,37 @@ const createdProject = async (scope, name, { slug, description, color }) => {
8433
8763
  organization
8434
8764
  };
8435
8765
  };
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);
8766
+ const logoHint = {
8767
+ ...projectHint,
8768
+ ...logoHints
8769
+ };
8770
+ const changedProject = async (api, id, change, logo = null) => {
8771
+ const params = { path: { id } };
8772
+ let project = await projectOf(api, id);
8773
+ if ([
8774
+ change.name,
8775
+ change.slug,
8776
+ change.description,
8777
+ change.color
8778
+ ].some((field) => field !== void 0)) {
8779
+ const body = {
8780
+ name: change.name ?? project.name,
8781
+ slug: change.slug ?? project.slug,
8782
+ description: change.description ?? project.description,
8783
+ color: change.color ?? project.color
8784
+ };
8785
+ validated(updateProjectBodySchema, body, "a change to a project");
8786
+ project = dataOf("change the project", await api.PATCH("/projects/{id}", {
8787
+ params,
8788
+ body
8789
+ }), projectHint);
8790
+ }
8791
+ if (logo !== null && logo !== "remove") project = dataOf("set the logo of the project", await api.PUT("/projects/{id}/logo", {
8792
+ params,
8793
+ ...logoUpload(logo)
8794
+ }), logoHint);
8795
+ if (logo === "remove") project = dataOf("remove the logo of the project", await api.DELETE("/projects/{id}/logo", { params }), logoHint);
8796
+ return project;
8449
8797
  };
8450
8798
  const deleteProject = async (api, id) => {
8451
8799
  doneWith("delete the project", await api.DELETE("/projects/{id}", { params: { path: { id } } }), projectHint);
@@ -8504,6 +8852,7 @@ const createProjectsCommand = (adaptersOf) => defineGroup({
8504
8852
  ["slug", shown.slug],
8505
8853
  ["description", shown.description === "" ? "-" : shown.description],
8506
8854
  ["color", shown.color],
8855
+ ["logo", shown.logoHash ?? "-"],
8507
8856
  ["docker", shown.dockerAllowed ? "allowed" : "not allowed"],
8508
8857
  ["members", String(shown.memberCount)],
8509
8858
  ["created", shown.createdAt],
@@ -8529,12 +8878,17 @@ const createProjectsCommand = (adaptersOf) => defineGroup({
8529
8878
  }),
8530
8879
  defineCommand({
8531
8880
  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],
8881
+ summary: "Change the name, slug, description, color or logo of a project.",
8882
+ usage: "projects update <project> [--name <name>] [--slug <slug>] [--description <text>] [--color <#rrggbb>] [--logo <path> | --no-logo]",
8883
+ optionsUsage: [
8884
+ ["--name <name>", "the new name"],
8885
+ ...fieldUsage,
8886
+ ...logoUsage
8887
+ ],
8535
8888
  options: {
8536
8889
  name: { type: "string" },
8537
- ...fieldOptions
8890
+ ...fieldOptions,
8891
+ ...logoOptions
8538
8892
  },
8539
8893
  positionals: {
8540
8894
  min: 1,
@@ -8547,10 +8901,11 @@ const createProjectsCommand = (adaptersOf) => defineGroup({
8547
8901
  values.slug,
8548
8902
  values.description,
8549
8903
  values.color
8550
- ].every((value) => value === void 0)) throw new UsageError("projects update needs --name, --slug, --description or --color.");
8904
+ ].every((value) => value === void 0) && !isLogoChange(values)) throw new UsageError("projects update needs --name, --slug, --description, --color, --logo or --no-logo.");
8905
+ const logo = await logoActionOf(invocation.context.workingDirectory, values);
8551
8906
  const run = await runOf(adaptersOf, invocation);
8552
8907
  const id = await resolveProjectId(run, invocation.positionals[0] ?? "");
8553
- const project = await changedProject(run.api, id, values);
8908
+ const project = await changedProject(run.api, id, values, logo);
8554
8909
  return printed(run, project, (shown) => `Changed ${shown.name} (${shown.slug}).`);
8555
8910
  }
8556
8911
  }),
@@ -9338,11 +9693,13 @@ const runtimeRowsOf = (runtime) => [
9338
9693
  ["agents", `${runtime.reconcile.agentsActive} active, ${runtime.reconcile.agentsQueued} queued`],
9339
9694
  ["log readers", String(runtime.ingest.readers)]
9340
9695
  ];
9696
+ const sessionsText = (count, noun) => `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
9697
+ const heldPoolsOf = ({ held = [] }) => held;
9341
9698
  const updateOf = ({ update }) => {
9342
9699
  if (update === null) return "not reported";
9343
9700
  switch (update.state) {
9344
9701
  case "current": return "current";
9345
- case "waiting": return `waits for ${String(update.sessions)} ${update.sessions === 1 ? "session" : "sessions"} to end before ${update.target ?? "-"}`;
9702
+ 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
9703
  case "updating": return `updating to ${update.target ?? "-"}`;
9347
9704
  case "failed": return `failed to reach ${update.target ?? "-"}, runs its previous version`;
9348
9705
  case "unmanaged": return "updated with its platform";
package/package.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "publishConfig": {
16
16
  "access": "public"
17
17
  },
18
- "version": "1.1.0",
18
+ "version": "1.2.0",
19
19
  "dependencies": {
20
20
  "@clack/prompts": "1.8.1",
21
21
  "@modelcontextprotocol/server": "2.1.0",