@kikimoradev/kcloud 0.13.0 → 0.15.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 (3) hide show
  1. package/README.md +17 -12
  2. package/dist/main.js +1680 -368
  3. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -431,9 +431,10 @@ const createTerminal = ({ stdout, stderr }) => ({
431
431
  //#region src/cli/cli.ts
432
432
  const program = "kcloud";
433
433
  const debugVariable = "KCLOUD_DEBUG";
434
+ const defaultOrigin = "https://cloud.kikimora.dev";
434
435
  const globalOptions = {
435
- profile: { type: "string" },
436
436
  url: { type: "string" },
437
+ org: { type: "string" },
437
438
  json: {
438
439
  type: "boolean",
439
440
  default: false
@@ -449,23 +450,28 @@ const globalOptions = {
449
450
  }
450
451
  };
451
452
  const globalOptionsUsage = [
452
- ["--profile <name>", "the saved instance to act on (default: the current one)"],
453
- ["--url <origin>", "the instance to act on, instead of the origin of the profile"],
453
+ ["--url <origin>", `the instance to act on (default: ${defaultOrigin})`],
454
+ ["--org <organization>", "the organization to act in, by slug, name or id"],
454
455
  ["--json", "print machine-readable output"],
455
456
  ["--yes", "ask nothing; every input comes from a flag, a file or stdin"],
456
457
  ["-h, --help", "show help"]
457
458
  ];
458
459
  const globalValuesSchema = z.looseObject({
459
- profile: z.string().optional(),
460
460
  url: z.string().optional(),
461
+ org: z.string().optional(),
461
462
  json: z.boolean(),
462
463
  yes: z.boolean()
463
464
  });
465
+ const organizationOf = (given) => {
466
+ if (given === void 0) return null;
467
+ if (given.trim() === "") throw new UsageError("--org needs an organization.", "Name it by its slug, name or id.");
468
+ return given.trim();
469
+ };
464
470
  const settingsOf = (values) => {
465
471
  const global = globalValuesSchema.parse(values);
466
472
  return {
467
- profile: global.profile ?? null,
468
473
  origin: global.url ?? null,
474
+ organization: organizationOf(global.org),
469
475
  json: global.json,
470
476
  yes: global.yes
471
477
  };
@@ -819,7 +825,8 @@ const workerSettingsSchema = z.strictObject({
819
825
  //#region ../../packages/contracts/src/common.ts
820
826
  const idSchema = z.uuid().toLowerCase();
821
827
  const localeSchema = z.enum(["en", "pl"]);
822
- const roleSchema = z.enum(["admin", "user"]);
828
+ const organizationRoleSchema = z.enum(["admin", "member"]);
829
+ const projectRoleSchema = z.enum(["admin", "member"]);
823
830
  const cursorSchema = z.string().max(512);
824
831
  const cursorQuerySchema = z.object({
825
832
  cursor: cursorSchema.optional(),
@@ -925,13 +932,16 @@ const errorCodes = Object.keys({
925
932
  "user.disabled": "forbidden",
926
933
  "user.cannot_modify_self": "rule_violation",
927
934
  "user.last_admin": "rule_violation",
935
+ "organization.not_found": "not_found",
936
+ "organization.invalid_slug": "rule_violation",
937
+ "organization.slug_taken": "conflict",
928
938
  "project.not_found": "not_found",
929
939
  "project.invalid_slug": "rule_violation",
930
940
  "project.slug_taken": "conflict",
931
- "project.member_exists": "conflict",
932
941
  "project.docker_not_allowed": "rule_violation",
933
942
  "project.member_not_found": "not_found",
934
943
  "project.member_must_be_active": "rule_violation",
944
+ "project.not_empty": "rule_violation",
935
945
  "credential.not_found": "not_found",
936
946
  "credential.label_taken": "conflict",
937
947
  "credential.in_use": "conflict",
@@ -978,6 +988,15 @@ const errorCodes = Object.keys({
978
988
  "runtime.monitor_draining": "conflict",
979
989
  "runtime.monitor_limited": "conflict",
980
990
  "runtime.monitor_check_unsupported": "rule_violation",
991
+ "node.not_found": "not_found",
992
+ "node.name_taken": "conflict",
993
+ "node.offline": "rule_violation",
994
+ "node.not_registered": "rule_violation",
995
+ "node.registration_invalid": "unauthenticated",
996
+ "node.registration_used": "conflict",
997
+ "node.in_use": "conflict",
998
+ "project.last_admin": "conflict",
999
+ "agent.node_unassigned": "rule_violation",
981
1000
  "stream.too_many": "rate_limited",
982
1001
  "infrastructure.not_configured": "rule_violation",
983
1002
  "infrastructure.provider_error": "internal",
@@ -1319,7 +1338,8 @@ z.object({
1319
1338
  unavailableReason: z.enum([
1320
1339
  "not_found",
1321
1340
  "tampered",
1322
- "too_large"
1341
+ "too_large",
1342
+ "node_offline"
1323
1343
  ]).nullable(),
1324
1344
  truncated: z.boolean()
1325
1345
  }).meta({ id: "TranscriptPage" });
@@ -1354,7 +1374,7 @@ const costPointSchema = z.object({
1354
1374
  costUsd: z.number().min(0),
1355
1375
  runs: z.int().nonnegative()
1356
1376
  }).meta({ id: "CostPoint" });
1357
- z.object({
1377
+ const costSeriesSchema = z.object({
1358
1378
  items: z.array(costPointSchema),
1359
1379
  totalCostUsd: z.number().nonnegative(),
1360
1380
  totalRuns: z.int().nonnegative(),
@@ -1364,6 +1384,12 @@ const costQuerySchema = z.object({
1364
1384
  from: z.iso.date(),
1365
1385
  to: z.iso.date()
1366
1386
  });
1387
+ const projectCostSchema = z.object({
1388
+ projectId: idSchema,
1389
+ costUsd: z.number().min(0),
1390
+ runs: z.int().nonnegative()
1391
+ }).meta({ id: "ProjectCost" });
1392
+ costSeriesSchema.extend({ projects: z.array(projectCostSchema) }).meta({ id: "OrganizationCosts" });
1367
1393
  pageSchema(z.object({
1368
1394
  taskId: z.string(),
1369
1395
  title: z.string(),
@@ -1376,23 +1402,142 @@ pageSchema(z.object({
1376
1402
  finishedAt: z.iso.datetime().nullable()
1377
1403
  }).meta({ id: "TaskCost" })).meta({ id: "TaskCostPage" });
1378
1404
  costQuerySchema.extend({ cursor: cursorSchema.optional() });
1405
+ const nodeStateSchema = z.enum([
1406
+ "pending",
1407
+ "online",
1408
+ "offline"
1409
+ ]);
1410
+ const nodeProviderSchema = z.enum(["self_hosted"]);
1411
+ const nodeNameSchema = z.string().trim().regex(slugPattern);
1412
+ const nodeHostSchema = z.object({
1413
+ cpus: z.number().nonnegative(),
1414
+ memoryBytes: z.number().nonnegative(),
1415
+ dataRoot: z.string(),
1416
+ serverVersion: z.string()
1417
+ }).meta({ id: "NodeHost" });
1418
+ const nodeCapabilitiesSchema = z.object({
1419
+ sandbox: z.boolean(),
1420
+ uidIsolation: z.boolean()
1421
+ }).meta({ id: "NodeCapabilities" });
1422
+ const nodeSchema = z.object({
1423
+ id: idSchema,
1424
+ name: nodeNameSchema,
1425
+ provider: nodeProviderSchema,
1426
+ state: nodeStateSchema,
1427
+ engine: z.enum(runtimeEngineKinds).nullable(),
1428
+ version: z.string().nullable(),
1429
+ host: nodeHostSchema.nullable(),
1430
+ capabilities: nodeCapabilitiesSchema.nullable(),
1431
+ agentCount: z.int().nonnegative(),
1432
+ connectedAt: z.iso.datetime().nullable(),
1433
+ lastSeenAt: z.iso.datetime().nullable(),
1434
+ registrationExpiresAt: z.iso.datetime().nullable(),
1435
+ createdAt: z.iso.datetime()
1436
+ }).meta({ id: "Node" });
1437
+ const nodeAgentSchema = z.object({
1438
+ id: idSchema,
1439
+ name: z.string(),
1440
+ slug: z.string(),
1441
+ projectId: idSchema,
1442
+ projectName: z.string(),
1443
+ state: agentStateSchema
1444
+ }).meta({ id: "NodeAgent" });
1445
+ const nodeRuntimeSchema = z.object({
1446
+ reportedAt: z.iso.datetime(),
1447
+ docker: z.object({
1448
+ state: z.enum([
1449
+ "ok",
1450
+ "unavailable",
1451
+ "disabled",
1452
+ "adopting"
1453
+ ]),
1454
+ lastError: z.string().nullable()
1455
+ }),
1456
+ storage: z.object({
1457
+ data: storageStateSchema,
1458
+ engine: storageStateSchema
1459
+ }),
1460
+ reconcile: z.object({
1461
+ lastTickAt: z.iso.datetime().nullable(),
1462
+ lastTickError: z.string().nullable(),
1463
+ agentsQueued: z.int().nonnegative(),
1464
+ agentsActive: z.int().nonnegative()
1465
+ }),
1466
+ ingest: z.object({ readers: z.int().nonnegative() }),
1467
+ sessions: z.object({
1468
+ monitor: z.object({
1469
+ effective: z.int().nonnegative(),
1470
+ inUse: z.int().nonnegative()
1471
+ }),
1472
+ executor: z.object({
1473
+ effective: z.int().nonnegative(),
1474
+ inUse: z.int().nonnegative()
1475
+ })
1476
+ })
1477
+ }).meta({ id: "NodeRuntime" });
1478
+ nodeSchema.extend({
1479
+ agents: z.array(nodeAgentSchema),
1480
+ runtime: nodeRuntimeSchema.nullable()
1481
+ }).meta({ id: "NodeDetail" });
1482
+ z.object({ items: z.array(nodeSchema) }).meta({ id: "NodeList" });
1483
+ z.object({ nodeId: idSchema });
1484
+ z.object({
1485
+ organizationId: idSchema,
1486
+ nodeId: idSchema
1487
+ });
1488
+ const createNodeBodySchema = z.object({ name: nodeNameSchema }).meta({ id: "CreateNodeBody" });
1489
+ const nodeRegistrationSchema = z.object({
1490
+ token: z.string(),
1491
+ expiresAt: z.iso.datetime()
1492
+ }).meta({ id: "NodeRegistration" });
1493
+ z.object({
1494
+ node: nodeSchema,
1495
+ registration: nodeRegistrationSchema
1496
+ }).meta({ id: "CreatedNode" }).meta({ id: "RenewedNodeRegistration" });
1497
+ z.object({
1498
+ token: z.string().min(1).max(256),
1499
+ version: z.string().min(1).max(128),
1500
+ engine: z.enum(runtimeEngineKinds),
1501
+ host: nodeHostSchema.nullable(),
1502
+ capabilities: nodeCapabilitiesSchema
1503
+ }).meta({ id: "RegisterNodeBody" });
1504
+ z.object({
1505
+ nodeId: idSchema,
1506
+ credential: z.string()
1507
+ }).meta({ id: "RegisteredNode" });
1379
1508
  //#endregion
1380
1509
  //#region ../../packages/contracts/src/identity.ts
1381
1510
  const emailSchema = z.email().max(254);
1382
1511
  const passwordSchema = z.string().min(12).max(128);
1383
1512
  const userStatusSchema = z.enum(["active", "disabled"]);
1384
- const meSchema = z.object({
1513
+ const organizationNameSchema = z.string().trim().min(2).max(80);
1514
+ const organizationSlugSchema = z.string().regex(slugPattern);
1515
+ const organizationSchema = z.object({
1385
1516
  id: idSchema,
1517
+ name: z.string(),
1518
+ slug: organizationSlugSchema,
1519
+ createdAt: z.iso.datetime()
1520
+ }).meta({ id: "Organization" });
1521
+ z.object({ items: z.array(organizationSchema) }).meta({ id: "OrganizationList" });
1522
+ z.object({ organizationId: idSchema });
1523
+ z.object({
1386
1524
  organizationId: idSchema,
1525
+ userId: idSchema
1526
+ });
1527
+ const meSchema = z.object({
1528
+ id: idSchema,
1387
1529
  email: emailSchema,
1388
- role: roleSchema,
1530
+ organization: organizationSchema,
1531
+ organizationRole: organizationRoleSchema,
1532
+ platformAdmin: z.boolean(),
1389
1533
  locale: localeSchema,
1390
1534
  mustChangePassword: z.boolean()
1391
1535
  }).meta({ id: "Me" });
1392
1536
  const userSchema = z.object({
1393
1537
  id: idSchema,
1394
1538
  email: emailSchema,
1395
- role: roleSchema,
1539
+ organizationRole: organizationRoleSchema,
1540
+ platformAdmin: z.boolean(),
1396
1541
  status: userStatusSchema,
1397
1542
  locale: localeSchema,
1398
1543
  mustChangePassword: z.boolean(),
@@ -1408,20 +1553,33 @@ z.object({
1408
1553
  newPassword: passwordSchema
1409
1554
  }).meta({ id: "ChangePasswordBody" });
1410
1555
  z.object({ locale: localeSchema }).meta({ id: "ChangeLocaleBody" });
1411
- z.object({
1556
+ const createUserBodySchema = z.object({
1412
1557
  email: emailSchema,
1413
- role: roleSchema,
1558
+ organizationRole: organizationRoleSchema,
1414
1559
  locale: localeSchema,
1415
1560
  initialPassword: passwordSchema.optional()
1416
1561
  }).meta({ id: "CreateUserBody" });
1417
- z.object({
1562
+ const createdUserSchema = z.object({
1418
1563
  user: userSchema,
1419
1564
  temporaryPassword: z.string().nullable()
1420
1565
  }).meta({ id: "CreatedUser" });
1421
- z.object({ role: roleSchema }).meta({ id: "UpdateUserBody" });
1566
+ z.object({ organizationRole: organizationRoleSchema }).meta({ id: "UpdateUserBody" });
1567
+ z.object({
1568
+ name: organizationNameSchema,
1569
+ slug: organizationSlugSchema.optional(),
1570
+ admin: z.object({
1571
+ email: emailSchema,
1572
+ locale: localeSchema,
1573
+ initialPassword: passwordSchema.optional()
1574
+ })
1575
+ }).meta({ id: "CreateOrganizationBody" });
1422
1576
  z.object({ temporaryPassword: z.string() }).meta({ id: "TemporaryPassword" });
1423
1577
  z.object({ items: z.array(userSchema) }).meta({ id: "UserList" });
1424
1578
  z.object({ id: idSchema });
1579
+ z.object({
1580
+ organization: organizationSchema,
1581
+ admin: createdUserSchema
1582
+ }).meta({ id: "CreatedOrganization" });
1425
1583
  const accessTokenNameSchema = z.string().trim().min(1).max(80);
1426
1584
  const accessTokenSchema = z.object({
1427
1585
  id: idSchema,
@@ -1481,6 +1639,8 @@ z.discriminatedUnion("status", [cliTokenWaitingSchema, cliTokenReadySchema]).met
1481
1639
  z.object({ version: z.string() }).meta({ id: "PlatformVersion" });
1482
1640
  const auditActionSchema = z.enum([
1483
1641
  "platform.bootstrapped",
1642
+ "organization.created",
1643
+ "organization.renamed",
1484
1644
  "user.created",
1485
1645
  "user.role_changed",
1486
1646
  "user.disabled",
@@ -1495,7 +1655,9 @@ const auditActionSchema = z.enum([
1495
1655
  "project.created",
1496
1656
  "project.updated",
1497
1657
  "project.member_added",
1658
+ "project.member_role_changed",
1498
1659
  "project.member_removed",
1660
+ "project.deleted",
1499
1661
  "project.docker_changed",
1500
1662
  "credential.created",
1501
1663
  "credential.rotated",
@@ -1526,6 +1688,10 @@ const auditActionSchema = z.enum([
1526
1688
  "node.volume_resized",
1527
1689
  "node.quiesced",
1528
1690
  "node.resumed",
1691
+ "node.created",
1692
+ "node.registered",
1693
+ "node.registration_renewed",
1694
+ "node.deleted",
1529
1695
  "master_key.reencrypted",
1530
1696
  "dead_letter.retried",
1531
1697
  "dead_letter.discarded"
@@ -1545,10 +1711,12 @@ const auditTargetTypeSchema = z.enum([
1545
1711
  "backup",
1546
1712
  "host_operation",
1547
1713
  "volume",
1548
- "queue"
1714
+ "queue",
1715
+ "node"
1549
1716
  ]);
1550
1717
  const auditEntrySchema = z.object({
1551
1718
  id: idSchema,
1719
+ organizationId: idSchema.nullable(),
1552
1720
  occurredAt: z.iso.datetime(),
1553
1721
  actor: z.object({
1554
1722
  kind: z.enum(["user", "system"]),
@@ -1565,6 +1733,7 @@ const auditEntrySchema = z.object({
1565
1733
  ip: z.string().nullable()
1566
1734
  }).meta({ id: "AuditEntry" });
1567
1735
  cursorQuerySchema.extend({
1736
+ organizationId: idSchema.optional(),
1568
1737
  action: auditActionSchema.optional(),
1569
1738
  actorUserId: idSchema.optional(),
1570
1739
  from: z.iso.datetime().optional(),
@@ -1577,6 +1746,7 @@ const slugSchema = z.string().regex(slugPattern);
1577
1746
  const colorSchema = z.string().regex(hexColorPattern);
1578
1747
  const projectSchema = z.object({
1579
1748
  id: idSchema,
1749
+ organizationId: idSchema,
1580
1750
  name: z.string(),
1581
1751
  slug: slugSchema,
1582
1752
  description: z.string(),
@@ -1589,26 +1759,27 @@ const projectSchema = z.object({
1589
1759
  const memberSchema = z.object({
1590
1760
  userId: idSchema,
1591
1761
  email: z.email(),
1592
- role: roleSchema,
1762
+ role: projectRoleSchema,
1593
1763
  status: userStatusSchema,
1594
1764
  addedAt: z.iso.datetime()
1595
1765
  }).meta({ id: "Member" });
1596
1766
  projectSchema.extend({ members: z.array(memberSchema) }).meta({ id: "ProjectDetail" });
1597
1767
  z.object({ items: z.array(projectSchema) }).meta({ id: "ProjectList" });
1598
1768
  z.object({ items: z.array(memberSchema) }).meta({ id: "MemberList" });
1599
- z.object({
1769
+ const createProjectBodySchema = z.object({
1600
1770
  name: z.string().trim().min(2).max(80),
1601
1771
  slug: slugSchema.optional(),
1602
1772
  description: z.string().max(500),
1603
1773
  color: colorSchema
1604
1774
  }).meta({ id: "CreateProjectBody" });
1605
- z.object({
1775
+ const updateProjectBodySchema = z.object({
1606
1776
  name: z.string().trim().min(2).max(80),
1607
1777
  slug: slugSchema,
1608
1778
  description: z.string().max(500),
1609
1779
  color: colorSchema
1610
1780
  }).meta({ id: "UpdateProjectBody" });
1611
1781
  z.object({ allowed: z.boolean() }).meta({ id: "SetProjectDockerBody" });
1782
+ z.object({ role: projectRoleSchema }).meta({ id: "SetProjectMemberBody" });
1612
1783
  z.object({ id: idSchema });
1613
1784
  z.object({
1614
1785
  id: idSchema,
@@ -2063,6 +2234,11 @@ const agentListItemSchema = z.object({
2063
2234
  availableActions: z.array(agentActionSchema),
2064
2235
  updatedAt: z.iso.datetime()
2065
2236
  }).meta({ id: "AgentListItem" });
2237
+ const agentNodeSchema = z.object({
2238
+ id: idSchema,
2239
+ name: nodeNameSchema,
2240
+ state: nodeStateSchema
2241
+ }).meta({ id: "AgentNode" });
2066
2242
  agentListItemSchema.extend({
2067
2243
  version: z.int().nonnegative(),
2068
2244
  description: z.string(),
@@ -2077,6 +2253,7 @@ agentListItemSchema.extend({
2077
2253
  toolAssignments: z.array(agentToolSchema),
2078
2254
  release: releaseSummarySchema.nullable(),
2079
2255
  runtime: agentRuntimeSchema,
2256
+ node: agentNodeSchema.nullable(),
2080
2257
  runtimeUid: z.int().positive().nullable(),
2081
2258
  sandbox: agentSandboxSchema.nullable(),
2082
2259
  createdAt: z.iso.datetime()
@@ -2099,6 +2276,7 @@ const createAgentBodySchema = z.object({
2099
2276
  slug: slugSchema.optional(),
2100
2277
  description: agentDescriptionSchema,
2101
2278
  kikimoraVersion: kikimoraVersionSchema,
2279
+ nodeId: idSchema,
2102
2280
  claudeCredentialId: idSchema,
2103
2281
  githubCredentialId: idSchema,
2104
2282
  repositories: agentRepositoryLinksSchema,
@@ -2370,7 +2548,7 @@ z.object({
2370
2548
  pending: z.array(idSchema)
2371
2549
  }),
2372
2550
  capacity: capacityForecastSchema
2373
- }).meta({ id: "Node" });
2551
+ }).meta({ id: "PlatformNode" });
2374
2552
  z.object({ role: volumeRoleSchema });
2375
2553
  z.object({ sizeGb: z.int().positive() }).meta({ id: "ResizeVolumeBody" });
2376
2554
  z.object({ agents: z.int().nonnegative() }).meta({ id: "QuiesceResult" });
@@ -2476,6 +2654,7 @@ z.object({
2476
2654
  version: z.string(),
2477
2655
  apiImage: installedImageSchema,
2478
2656
  webImage: installedImageSchema,
2657
+ nodeImage: installedImageSchema,
2479
2658
  latest: z.string().nullable(),
2480
2659
  latestError: registryUnavailableSchema.nullable(),
2481
2660
  versions: z.array(z.string()),
@@ -2566,11 +2745,16 @@ const agentUsageSchema = z.object({
2566
2745
  blockReadBytes: byteCountSchema,
2567
2746
  blockWriteBytes: byteCountSchema
2568
2747
  }).meta({ id: "AgentUsage" });
2748
+ const nodeHostSeriesSchema = z.object({
2749
+ nodeId: idSchema,
2750
+ nodeName: z.string(),
2751
+ points: z.array(hostPointSchema)
2752
+ }).meta({ id: "NodeHostSeries" });
2569
2753
  z.object({
2570
2754
  resolution: metricsResolutionSchema,
2571
2755
  from: instantSchema,
2572
2756
  to: instantSchema,
2573
- host: z.array(hostPointSchema),
2757
+ nodes: z.array(nodeHostSeriesSchema),
2574
2758
  agents: z.array(agentUsageSchema)
2575
2759
  }).meta({ id: "HostMetrics" });
2576
2760
  z.object({
@@ -2659,7 +2843,11 @@ z.strictObject({ label: textParam }), z.strictObject({
2659
2843
  }), z.strictObject({
2660
2844
  agentName: textParam,
2661
2845
  failureCount: countParam
2662
- }), z.strictObject({ agentName: textParam }), z.strictObject({ ratio: ratioParam }), z.strictObject({
2846
+ }), z.strictObject({ agentName: textParam }), z.strictObject({
2847
+ node: textParam,
2848
+ ratio: ratioParam
2849
+ }), z.strictObject({
2850
+ node: textParam,
2663
2851
  filesystem: filesystemNameSchema,
2664
2852
  ratio: ratioParam
2665
2853
  }), z.strictObject({ backupKind: backupKindSchema }), z.strictObject({ hours: z.int().positive() }), z.strictObject({
@@ -2842,10 +3030,90 @@ const createAdapters = (context) => ({
2842
3030
  clientName: () => `kcloud on ${hostname()}`
2843
3031
  });
2844
3032
  //#endregion
2845
- //#region src/profile/config.ts
3033
+ //#region src/resolve.ts
3034
+ const organizationHints = { "organization.not_found": "kcloud orgs list shows the organizations you can see." };
3035
+ const isId = (reference) => idSchema.safeParse(reference).success;
3036
+ const once = (load) => {
3037
+ let loaded = null;
3038
+ return () => loaded ??= load();
3039
+ };
3040
+ const slugsOf = (items) => items.map(({ slug }) => slug).join(", ");
3041
+ const organizationIn = (organizations, reference) => {
3042
+ const found = organizations.filter((organization) => organization.id === reference.toLowerCase() || organization.slug === reference || organization.name === reference);
3043
+ const [first] = found;
3044
+ if (first === void 0) throw new CliError(`There is no organization ${reference} you can see.`, organizations.length === 0 ? "kcloud orgs list shows the organizations you can see." : `kcloud orgs list shows ${slugsOf(organizations)}.`);
3045
+ if (found.length > 1) throw new CliError(`${reference} names ${found.length} organizations.`, "Name it by its slug or by its id instead.");
3046
+ return first;
3047
+ };
3048
+ const scopeOf = (api, reference) => {
3049
+ const me = once(async () => dataOf("say who the token belongs to", await api.GET("/auth/me")));
3050
+ const organizations = once(async () => dataOf("list the organizations", await api.GET("/organizations")).items);
3051
+ return {
3052
+ api,
3053
+ me,
3054
+ organizations,
3055
+ organization: once(async () => reference === null ? (await me()).organization : organizationIn(await organizations(), reference))
3056
+ };
3057
+ };
3058
+ const projectsOf = async (scope) => {
3059
+ const { id } = await scope.organization();
3060
+ return dataOf("list the projects", await scope.api.GET("/organizations/{organizationId}/projects", { params: { path: { organizationId: id } } }), organizationHints).items;
3061
+ };
3062
+ const matches = (reference, project) => project.slug === reference || project.name === reference;
3063
+ const resolveProjectId = async (scope, reference) => {
3064
+ if (isId(reference)) return reference;
3065
+ const projects = await projectsOf(scope);
3066
+ const found = projects.filter((project) => matches(reference, project));
3067
+ const [first] = found;
3068
+ if (first === void 0) {
3069
+ const { slug } = await scope.organization();
3070
+ throw new CliError(`There is no project ${reference} you can see in ${slug}.`, projects.length === 0 ? "kcloud projects list shows the projects you are a member of." : `kcloud projects list shows ${slugsOf(projects)}.`);
3071
+ }
3072
+ if (found.length > 1) throw new CliError(`${reference} names ${found.length} projects.`, "Name it by its slug or by its id instead.");
3073
+ return first.id;
3074
+ };
3075
+ const agentsOfProject = async (api, projectId) => dataOf("list the agents of the project", await api.GET("/projects/{id}/agents", { params: { path: { id: projectId } } })).items;
3076
+ const resolveAgentId = async (scope, reference, project) => {
3077
+ if (isId(reference)) return reference;
3078
+ const [left, right] = reference.split("/");
3079
+ const projectReference = right === void 0 ? project : left;
3080
+ const agentReference = right ?? left ?? "";
3081
+ const searched = projectReference === void 0 ? await projectsOf(scope) : [{
3082
+ id: await resolveProjectId(scope, projectReference),
3083
+ slug: projectReference
3084
+ }];
3085
+ const found = [];
3086
+ for (const candidate of searched) for (const agent of await agentsOfProject(scope.api, candidate.id)) if (agent.slug === agentReference || agent.name === agentReference) found.push({
3087
+ agent,
3088
+ project: candidate
3089
+ });
3090
+ const [first] = found;
3091
+ if (first === void 0) throw new CliError(`There is no agent ${reference} you can see.`, "Name it as <project>/<agent>, or pass --project; kcloud agents list shows them.");
3092
+ if (found.length > 1) throw new CliError(`${reference} names an agent in ${found.map(({ project }) => project.slug).join(", ")}.`, "Name it as <project>/<agent> instead.");
3093
+ return first.agent.id;
3094
+ };
3095
+ const sameEmail = (left, right) => left.toLowerCase() === right.toLowerCase();
3096
+ const organizationMembersOf = async (api, organizationId) => dataOf("list the members of the organization", await api.GET("/organizations/{organizationId}/members", { params: { path: { organizationId } } }), organizationHints).items;
3097
+ const resolveOrganizationMember = async (api, organizationId, reference) => {
3098
+ const found = (await organizationMembersOf(api, organizationId)).find((member) => member.id === reference.toLowerCase() || sameEmail(member.email, reference));
3099
+ if (found === void 0) throw new CliError(`The organization has no member ${reference}.`, "kcloud orgs members list shows them.");
3100
+ return found;
3101
+ };
3102
+ //#endregion
3103
+ //#region src/instance/config.ts
2846
3104
  const configFileName = "cli.json";
2847
3105
  const stateDirectoryName = ".kikimora-cloud";
2848
- const profileFileOf = (homeDirectory) => join(homeDirectory, stateDirectoryName, configFileName);
3106
+ const configFileOf = (homeDirectory) => join(homeDirectory, stateDirectoryName, configFileName);
3107
+ const instanceSchema = z.strictObject({
3108
+ token: z.string().min(1),
3109
+ tokenId: z.string().min(1).nullable(),
3110
+ email: z.string().min(1).nullable(),
3111
+ organization: z.string().min(1).nullable()
3112
+ });
3113
+ const instanceStoreSchema = z.strictObject({
3114
+ version: z.literal(2),
3115
+ instances: z.record(z.url(), instanceSchema)
3116
+ });
2849
3117
  const profileSchema = z.strictObject({
2850
3118
  origin: z.url(),
2851
3119
  token: z.string().min(1),
@@ -2857,97 +3125,88 @@ const profileStoreSchema = z.strictObject({
2857
3125
  current: z.string().min(1).nullable(),
2858
3126
  profiles: z.record(z.string().min(1), profileSchema)
2859
3127
  });
2860
- const emptyProfileStore = {
2861
- version: 1,
2862
- current: null,
2863
- profiles: {}
3128
+ const storedSchema = z.discriminatedUnion("version", [instanceStoreSchema, profileStoreSchema]);
3129
+ const emptyInstanceStore = {
3130
+ version: 2,
3131
+ instances: {}
2864
3132
  };
2865
- const readProfileStore = async (homeDirectory) => {
2866
- const path = profileFileOf(homeDirectory);
3133
+ const instancesOfProfiles = ({ current, profiles }) => {
3134
+ const currentLast = Object.entries(profiles).toSorted(([left], [right]) => Number(left === current) - Number(right === current));
3135
+ return {
3136
+ version: 2,
3137
+ instances: Object.fromEntries(currentLast.map(([, { origin, token, tokenId, email }]) => [new URL(origin).origin, {
3138
+ token,
3139
+ tokenId,
3140
+ email,
3141
+ organization: null
3142
+ }]))
3143
+ };
3144
+ };
3145
+ const readInstanceStore = async (homeDirectory) => {
3146
+ const path = configFileOf(homeDirectory);
2867
3147
  const content = await readPrivateFile(path);
2868
- if (content === null) return emptyProfileStore;
3148
+ if (content === null) return emptyInstanceStore;
2869
3149
  let document;
2870
3150
  try {
2871
3151
  document = JSON.parse(content);
2872
3152
  } catch {
2873
3153
  throw new CliError(`${path} is not JSON.`, `Fix it or delete it, then log in again.`);
2874
3154
  }
2875
- const parsed = profileStoreSchema.safeParse(document);
3155
+ const parsed = storedSchema.safeParse(document);
2876
3156
  if (!parsed.success) throw new CliError(`${path} is not what ${configFileName} should hold.`, z.prettifyError(parsed.error));
2877
- return parsed.data;
3157
+ return parsed.data.version === 2 ? parsed.data : instancesOfProfiles(parsed.data);
2878
3158
  };
2879
- const writeProfileStore = async (homeDirectory, store) => {
2880
- await writePrivateFile(profileFileOf(homeDirectory), `${JSON.stringify(store, null, 2)}\n`);
3159
+ const writeInstanceStore = async (homeDirectory, store) => {
3160
+ await writePrivateFile(configFileOf(homeDirectory), `${JSON.stringify(store, null, 2)}\n`);
2881
3161
  };
2882
- const withProfile = (store, name, profile) => ({
2883
- version: 1,
2884
- current: name,
2885
- profiles: {
2886
- ...store.profiles,
2887
- [name]: profile
3162
+ const withInstance = (store, origin, instance) => ({
3163
+ version: 2,
3164
+ instances: {
3165
+ ...store.instances,
3166
+ [origin]: instance
2888
3167
  }
2889
3168
  });
2890
- const withoutProfile = (store, name) => {
2891
- const profiles = Object.fromEntries(Object.entries(store.profiles).filter(([candidate]) => candidate !== name));
2892
- const remaining = Object.keys(profiles);
2893
- return {
2894
- version: 1,
2895
- current: store.current === name ? remaining[0] ?? null : store.current,
2896
- profiles
2897
- };
2898
- };
2899
- const profileNameOf = (origin) => {
2900
- const { hostname } = new URL(origin);
2901
- return hostname;
2902
- };
3169
+ const withoutInstance = (store, origin) => ({
3170
+ version: 2,
3171
+ instances: Object.fromEntries(Object.entries(store.instances).filter(([candidate]) => candidate !== origin))
3172
+ });
2903
3173
  //#endregion
2904
- //#region src/profile/session.ts
3174
+ //#region src/instance/session.ts
2905
3175
  const urlVariable = "KIKIMORA_CLOUD_URL";
2906
3176
  const tokenVariable = "KIKIMORA_CLOUD_TOKEN";
2907
- const notSignedIn = () => new CliError("No instance is signed in here.", `Run kcloud login --url <origin>, or set ${urlVariable} and ${tokenVariable}.`);
2908
- const namedProfileOf = (store, name) => {
2909
- const profile = store.profiles[name];
2910
- if (profile === void 0) {
2911
- const known = Object.keys(store.profiles);
2912
- throw new CliError(`There is no profile ${name} here.`, known.length === 0 ? "Run kcloud login --url <origin> to make one." : `kcloud profiles list shows ${known.join(", ")}.`);
2913
- }
2914
- return [name, profile];
3177
+ const organizationVariable = "KIKIMORA_CLOUD_ORG";
3178
+ const givenIn = (environment, name) => {
3179
+ const value = environment[name]?.trim();
3180
+ return value === void 0 || value === "" ? null : value;
2915
3181
  };
2916
- const currentProfileOf = (store) => {
2917
- if (store.current === null) throw notSignedIn();
2918
- return namedProfileOf(store, store.current);
3182
+ const originFor = (settings, context) => originOf(settings.origin ?? givenIn(context.environment, "KIKIMORA_CLOUD_URL") ?? "https://cloud.kikimora.dev");
3183
+ const notSignedIn = (origin, store) => {
3184
+ const known = Object.keys(store.instances);
3185
+ return new CliError(`Not signed in to ${origin}.`, known.length === 0 ? "Run kcloud login (add --url for another instance)." : `Run kcloud login, or add --url for ${known.join(" or ")}.`);
2919
3186
  };
2920
3187
  const openSession = async ({ settings, context, api = createApi }) => {
2921
- const store = await readProfileStore(context.homeDirectory);
2922
- const fromEnvironment = context.environment[tokenVariable];
2923
- if (fromEnvironment !== void 0 && fromEnvironment !== "") {
2924
- const origin = settings.origin ?? context.environment["KIKIMORA_CLOUD_URL"] ?? (settings.profile === null ? store.current === null ? null : currentProfileOf(store)[1].origin : namedProfileOf(store, settings.profile)[1].origin);
2925
- if (origin === null || origin === "") throw new CliError(`${tokenVariable} is set without an instance to use it on.`, `Pass --url <origin> or set ${urlVariable}.`);
2926
- return {
2927
- api: api({
2928
- origin: originOf(origin),
2929
- token: fromEnvironment
2930
- }),
2931
- origin: originOf(origin),
2932
- profileName: null,
2933
- store
2934
- };
2935
- }
2936
- const [name, profile] = settings.profile === null ? currentProfileOf(store) : namedProfileOf(store, settings.profile);
2937
- const origin = originOf(settings.origin ?? profile.origin);
3188
+ const origin = originFor(settings, context);
3189
+ const store = await readInstanceStore(context.homeDirectory);
3190
+ const stored = store.instances[origin] ?? null;
3191
+ const fromEnvironment = givenIn(context.environment, tokenVariable);
3192
+ const token = fromEnvironment ?? stored?.token ?? null;
3193
+ if (token === null) throw notSignedIn(origin, store);
3194
+ const organization = settings.organization ?? givenIn(context.environment, "KIKIMORA_CLOUD_ORG") ?? stored?.organization ?? null;
2938
3195
  return {
2939
- api: api({
3196
+ ...scopeOf(api({
2940
3197
  origin,
2941
- token: profile.token
2942
- }),
3198
+ token
3199
+ }), organization),
2943
3200
  origin,
2944
- profileName: name,
2945
- store
3201
+ token,
3202
+ tokenFromEnvironment: fromEnvironment !== null,
3203
+ store,
3204
+ stored
2946
3205
  };
2947
3206
  };
2948
3207
  //#endregion
2949
3208
  //#region src/commands/auth/account.ts
2950
- const passwordLength = `between ${passwordSchema.minLength ?? 0} and ${passwordSchema.maxLength ?? 0} characters`;
3209
+ const passwordLength$1 = `between ${passwordSchema.minLength ?? 0} and ${passwordSchema.maxLength ?? 0} characters`;
2951
3210
  const createAuthCommand = (adaptersOf) => defineGroup({
2952
3211
  name: "auth",
2953
3212
  summary: "Your own account on this instance.",
@@ -2975,8 +3234,8 @@ const createAuthCommand = (adaptersOf) => defineGroup({
2975
3234
  validate: (value) => value === "" ? "cannot be empty" : void 0
2976
3235
  });
2977
3236
  const newPassword = await prompt.password({
2978
- message: `Your new password, ${passwordLength}`,
2979
- validate: (value) => passwordSchema.safeParse(value).success ? void 0 : passwordLength
3237
+ message: `Your new password, ${passwordLength$1}`,
3238
+ validate: (value) => passwordSchema.safeParse(value).success ? void 0 : passwordLength$1
2980
3239
  });
2981
3240
  doneWith("change the password", await session.api.PATCH("/auth/me/password", { body: {
2982
3241
  currentPassword,
@@ -3019,16 +3278,7 @@ const createAuthCommand = (adaptersOf) => defineGroup({
3019
3278
  });
3020
3279
  //#endregion
3021
3280
  //#region src/commands/auth/login.ts
3022
- const usage = "login [--url <origin>] [--with-token] [--profile <name>]";
3023
- const originFor = async (settings, context) => {
3024
- const given = settings.origin ?? context.environment["KIKIMORA_CLOUD_URL"];
3025
- if (given !== void 0 && given !== "") return originOf(given);
3026
- const store = await readProfileStore(context.homeDirectory);
3027
- const named = settings.profile ?? store.current;
3028
- const known = named === null ? void 0 : store.profiles[named];
3029
- if (known !== void 0) return originOf(known.origin);
3030
- throw new UsageError("login needs the address of an instance.", `Pass --url <origin> or set ${urlVariable}.`);
3031
- };
3281
+ const usage = "login [--url <origin>] [--with-token]";
3032
3282
  const isTerminal$2 = (stream) => stream.isTTY === true;
3033
3283
  const pipedText$2 = async (context) => {
3034
3284
  if (isTerminal$2(context.io.stdin)) return "";
@@ -3037,8 +3287,8 @@ const pipedText$2 = async (context) => {
3037
3287
  return Buffer.concat(chunks).toString("utf8").trim();
3038
3288
  };
3039
3289
  const readToken = async (context, settings) => {
3040
- const fromEnvironment = context.environment[tokenVariable];
3041
- if (fromEnvironment !== void 0 && fromEnvironment !== "") return fromEnvironment;
3290
+ const fromEnvironment = givenIn(context.environment, tokenVariable);
3291
+ if (fromEnvironment !== null) return fromEnvironment;
3042
3292
  const piped = await pipedText$2(context);
3043
3293
  if (piped !== "") return piped;
3044
3294
  if (settings.yes) throw new UsageError("login --with-token found no token to use.", `Pipe it on stdin or set ${tokenVariable}.`);
@@ -3046,9 +3296,13 @@ const readToken = async (context, settings) => {
3046
3296
  };
3047
3297
  const createLoginCommand = (adaptersOf) => defineCommand({
3048
3298
  name: "login",
3049
- summary: "Sign in to an instance and save its token in a profile.",
3299
+ summary: "Sign in to an instance and save its token for its address.",
3050
3300
  usage,
3051
- optionsUsage: [["--with-token", "take a token from stdin or the environment instead of a browser"], ["", `the address comes from --url or ${urlVariable}, the token of --with-token from stdin or ${tokenVariable}`]],
3301
+ optionsUsage: [
3302
+ ["--with-token", "take a token from stdin or the environment instead of a browser"],
3303
+ ["", `the address comes from --url or ${urlVariable}, or is ${defaultOrigin}`],
3304
+ ["", `the token of --with-token comes from stdin or ${tokenVariable}`]
3305
+ ],
3052
3306
  options: { "with-token": {
3053
3307
  type: "boolean",
3054
3308
  default: false
@@ -3059,7 +3313,7 @@ const createLoginCommand = (adaptersOf) => defineCommand({
3059
3313
  },
3060
3314
  run: async ({ values, settings, context }) => {
3061
3315
  const adapters = adaptersOf(context);
3062
- const origin = await originFor(settings, context);
3316
+ const origin = originFor(settings, context);
3063
3317
  const prompt = adapters.prompter(settings);
3064
3318
  const anonymous = adapters.api({
3065
3319
  origin,
@@ -3079,16 +3333,16 @@ const createLoginCommand = (adaptersOf) => defineCommand({
3079
3333
  const me = dataOf("say who the token belongs to", await identified.GET("/auth/me"));
3080
3334
  await save({
3081
3335
  context,
3082
- settings,
3083
3336
  origin,
3084
3337
  secret,
3085
3338
  me
3086
3339
  });
3087
3340
  const version = await instanceVersion(identified);
3088
- prompt.outro(`Signed in to ${origin} as ${me.email}${version === null ? "" : ` on ${version}`}.`);
3341
+ prompt.outro(`Signed in to ${origin} as ${me.email} of ${me.organization.name}${version === null ? "" : ` on ${version}`}.`);
3089
3342
  if (settings.json) context.terminal.json({
3090
3343
  origin,
3091
3344
  email: me.email,
3345
+ organization: me.organization.slug,
3092
3346
  version
3093
3347
  });
3094
3348
  return exitCodes.ok;
@@ -3124,14 +3378,14 @@ const authorizeInBrowser = async ({ adapters, api, origin, prompt }) => {
3124
3378
  }
3125
3379
  }
3126
3380
  };
3127
- const save = async ({ context, settings, origin, secret, me }) => {
3128
- const store = await readProfileStore(context.homeDirectory);
3129
- const name = settings.profile ?? profileNameOf(origin);
3130
- await writeProfileStore(context.homeDirectory, withProfile(store, name, {
3131
- origin,
3381
+ const save = async ({ context, origin, secret, me }) => {
3382
+ const store = await readInstanceStore(context.homeDirectory);
3383
+ const previous = store.instances[origin];
3384
+ await writeInstanceStore(context.homeDirectory, withInstance(store, origin, {
3132
3385
  token: secret,
3133
3386
  tokenId: null,
3134
- email: me.email
3387
+ email: me.email,
3388
+ organization: previous?.email === me.email ? previous.organization : null
3135
3389
  }));
3136
3390
  };
3137
3391
  const instanceVersion = async (api) => {
@@ -3140,11 +3394,15 @@ const instanceVersion = async (api) => {
3140
3394
  };
3141
3395
  //#endregion
3142
3396
  //#region src/commands/auth/logout.ts
3397
+ const outcomeOf = (session, kept) => {
3398
+ if (session.tokenFromEnvironment) return kept ? `${tokenVariable} signs in to ${session.origin}, so nothing is stored here to forget.` : `Revoked the token of ${tokenVariable} on ${session.origin}.`;
3399
+ return kept ? `Forgot ${session.origin} here. Its token is still usable.` : `Revoked the token of ${session.origin} and forgot it here.`;
3400
+ };
3143
3401
  const createLogoutCommand = (adaptersOf) => defineCommand({
3144
3402
  name: "logout",
3145
- summary: "Revoke the token of a profile and forget it.",
3146
- usage: "logout [--keep-token]",
3147
- optionsUsage: [["--keep-token", "forget the profile here and leave its token usable elsewhere"]],
3403
+ summary: "Revoke the token of an instance and forget it here.",
3404
+ usage: "logout [--url <origin>] [--keep-token]",
3405
+ optionsUsage: [["--keep-token", "forget the instance here and leave its token usable elsewhere"]],
3148
3406
  options: { "keep-token": {
3149
3407
  type: "boolean",
3150
3408
  default: false
@@ -3160,12 +3418,10 @@ const createLogoutCommand = (adaptersOf) => defineCommand({
3160
3418
  context,
3161
3419
  api: adapters.api
3162
3420
  });
3163
- if (!values["keep-token"]) doneWith("revoke the token", await session.api.POST("/auth/logout"));
3164
- if (session.profileName !== null) {
3165
- const store = await readProfileStore(context.homeDirectory);
3166
- await writeProfileStore(context.homeDirectory, withoutProfile(store, session.profileName));
3167
- }
3168
- context.terminal.line(values["keep-token"] ? `Forgot ${session.origin} here; its token is still usable.` : `Revoked the token of ${session.origin} and forgot it here.`);
3421
+ const kept = values["keep-token"];
3422
+ if (!kept) doneWith("revoke the token", await session.api.POST("/auth/logout"));
3423
+ if (!session.tokenFromEnvironment) await writeInstanceStore(context.homeDirectory, withoutInstance(session.store, session.origin));
3424
+ context.terminal.line(outcomeOf(session, kept));
3169
3425
  return exitCodes.ok;
3170
3426
  }
3171
3427
  });
@@ -3228,9 +3484,10 @@ const createVersionCommand = (adaptersOf, cliVersion) => defineCommand({
3228
3484
  });
3229
3485
  //#endregion
3230
3486
  //#region src/commands/auth/whoami.ts
3487
+ const organizationNameOf = ({ name, slug }) => `${name} (${slug})`;
3231
3488
  const createWhoamiCommand = (adaptersOf) => defineCommand({
3232
3489
  name: "whoami",
3233
- summary: "Say which instance and which account the token belongs to.",
3490
+ summary: "Say which instance, account and organization kcloud acts with.",
3234
3491
  usage: "whoami",
3235
3492
  options: {},
3236
3493
  positionals: {
@@ -3244,20 +3501,23 @@ const createWhoamiCommand = (adaptersOf) => defineCommand({
3244
3501
  context,
3245
3502
  api: adapters.api
3246
3503
  });
3247
- const me = dataOf("say who the token belongs to", await session.api.GET("/auth/me"));
3504
+ const me = await session.me();
3505
+ const actsIn = await session.organization();
3248
3506
  if (settings.json) {
3249
3507
  context.terminal.json({
3250
3508
  origin: session.origin,
3251
- profile: session.profileName,
3252
- ...me
3509
+ ...me,
3510
+ actsIn
3253
3511
  });
3254
3512
  return exitCodes.ok;
3255
3513
  }
3256
3514
  context.terminal.line(renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
3257
3515
  ["instance", session.origin],
3258
- ["profile", session.profileName ?? "the environment"],
3259
3516
  ["email", me.email],
3260
- ["role", me.role],
3517
+ ["organization", organizationNameOf(me.organization)],
3518
+ ["role", me.organizationRole],
3519
+ ["platform admin", me.platformAdmin ? "yes" : "no"],
3520
+ ["acts in", organizationNameOf(actsIn)],
3261
3521
  ["locale", me.locale]
3262
3522
  ]));
3263
3523
  return exitCodes.ok;
@@ -3274,6 +3534,9 @@ const runOf = async (adaptersOf, { settings, context }) => {
3274
3534
  });
3275
3535
  return {
3276
3536
  api: session.api,
3537
+ me: session.me,
3538
+ organizations: session.organizations,
3539
+ organization: session.organization,
3277
3540
  origin: session.origin,
3278
3541
  adapters,
3279
3542
  prompt: adapters.prompter(settings),
@@ -3282,40 +3545,6 @@ const runOf = async (adaptersOf, { settings, context }) => {
3282
3545
  };
3283
3546
  };
3284
3547
  //#endregion
3285
- //#region src/resolve.ts
3286
- const isId = (reference) => idSchema.safeParse(reference).success;
3287
- const matches = (reference, project) => project.slug === reference || project.name === reference;
3288
- const projectsOf = async (api) => dataOf("list the projects", await api.GET("/projects")).items;
3289
- const resolveProjectId = async (api, reference) => {
3290
- if (isId(reference)) return reference;
3291
- const projects = await projectsOf(api);
3292
- const found = projects.filter((project) => matches(reference, project));
3293
- const [first] = found;
3294
- if (first === void 0) throw new CliError(`There is no project ${reference} you can see.`, projects.length === 0 ? "kcloud projects list shows the projects you are a member of." : `kcloud projects list shows ${projects.map(({ slug }) => slug).join(", ")}.`);
3295
- if (found.length > 1) throw new CliError(`${reference} names ${found.length} projects.`, "Name it by its slug or by its id instead.");
3296
- return first.id;
3297
- };
3298
- const agentsOfProject = async (api, projectId) => dataOf("list the agents of the project", await api.GET("/projects/{id}/agents", { params: { path: { id: projectId } } })).items;
3299
- const resolveAgentId = async (api, reference, project) => {
3300
- if (isId(reference)) return reference;
3301
- const [left, right] = reference.split("/");
3302
- const projectReference = right === void 0 ? project : left;
3303
- const agentReference = right ?? left ?? "";
3304
- const searched = projectReference === void 0 ? await projectsOf(api) : [{
3305
- id: await resolveProjectId(api, projectReference),
3306
- slug: projectReference
3307
- }];
3308
- const found = [];
3309
- for (const candidate of searched) for (const agent of await agentsOfProject(api, candidate.id)) if (agent.slug === agentReference || agent.name === agentReference) found.push({
3310
- agent,
3311
- project: candidate
3312
- });
3313
- const [first] = found;
3314
- if (first === void 0) throw new CliError(`There is no agent ${reference} you can see.`, "Name it as <project>/<agent>, or pass --project; kcloud agents list shows them.");
3315
- if (found.length > 1) throw new CliError(`${reference} names an agent in ${found.map(({ project }) => project.slug).join(", ")}.`, "Name it as <project>/<agent> instead.");
3316
- return first.agent.id;
3317
- };
3318
- //#endregion
3319
3548
  //#region src/commands/agents/shared.ts
3320
3549
  const agentOption = { project: { type: "string" } };
3321
3550
  const agentUsage = [["--project <name>", "the project of the agent, when the agent is named alone"]];
@@ -3324,11 +3553,12 @@ const agentHints = {
3324
3553
  "project.not_found": "kcloud projects list shows the projects you are a member of.",
3325
3554
  "agent.invalid_transition": "kcloud agents show <agent> lists the actions it takes now.",
3326
3555
  "agent.busy": "The agent is working on another action; try again in a moment.",
3327
- "concurrency.conflict": "Someone changed the agent meanwhile; read it again and retry."
3556
+ "concurrency.conflict": "Someone changed the agent meanwhile; read it again and retry.",
3557
+ "node.offline": "The node of the agent is not connected; kcloud nodes show <node> tells when it was last seen."
3328
3558
  };
3329
3559
  const agentFrom = async (run, reference, project) => {
3330
3560
  if (reference === void 0 || reference === "") throw new UsageError("This command needs an agent.", "Name it as <project>/<agent>, or pass --project.");
3331
- return resolveAgentId(run.api, reference, project);
3561
+ return resolveAgentId(run, reference, project);
3332
3562
  };
3333
3563
  //#endregion
3334
3564
  //#region src/commands/agents/check.ts
@@ -3403,6 +3633,7 @@ const agentPlanOf = (agent) => ({
3403
3633
  slug: agent.slug,
3404
3634
  description: agent.description,
3405
3635
  kikimoraVersion: agent.kikimoraVersion,
3636
+ ...agent.node === null ? {} : { nodeId: agent.node.id },
3406
3637
  claudeCredentialId: agent.claudeCredential.id,
3407
3638
  githubCredentialId: agent.githubCredential.id,
3408
3639
  repositories: agent.repositories.map(({ repositoryId, directory }) => ({
@@ -3420,17 +3651,17 @@ const agentPlanOf = (agent) => ({
3420
3651
  });
3421
3652
  //#endregion
3422
3653
  //#region src/commands/agents/config.ts
3423
- const agentsOf = async (api, project) => {
3424
- const projects = project === void 0 ? (await projectsOf(api)).map(({ id, slug }) => ({
3654
+ const agentsOf = async (scope, project) => {
3655
+ const projects = project === void 0 ? (await projectsOf(scope)).map(({ id, slug }) => ({
3425
3656
  id,
3426
3657
  slug
3427
3658
  })) : [{
3428
- id: await resolveProjectId(api, project),
3659
+ id: await resolveProjectId(scope, project),
3429
3660
  slug: project
3430
3661
  }];
3431
3662
  const listed = [];
3432
3663
  for (const { id, slug } of projects) {
3433
- const { items } = dataOf("list the agents of the project", await api.GET("/projects/{id}/agents", { params: { path: { id } } }), agentHints);
3664
+ const { items } = dataOf("list the agents of the project", await scope.api.GET("/projects/{id}/agents", { params: { path: { id } } }), agentHints);
3434
3665
  for (const agent of items) listed.push({
3435
3666
  project: slug,
3436
3667
  ...agent
@@ -3463,7 +3694,7 @@ const configCommands = (adaptersOf) => [
3463
3694
  },
3464
3695
  run: async (invocation) => {
3465
3696
  const run = await runOf(adaptersOf, invocation);
3466
- const listed = await agentsOf(run.api, invocation.values.project);
3697
+ const listed = await agentsOf(run, invocation.values.project);
3467
3698
  return printed(run, listed, (items) => items.length === 0 ? "There is no agent here yet; kcloud agents create makes one." : renderTable([
3468
3699
  { header: "PROJECT" },
3469
3700
  { header: "AGENT" },
@@ -3581,6 +3812,7 @@ const detailsOf = (agent) => [
3581
3812
  ["wanted", agent.desiredState],
3582
3813
  ["observed", agent.observedState],
3583
3814
  ["release", agent.kikimoraVersion],
3815
+ ["node", agent.node === null ? "none" : `${agent.node.name} (${agent.node.state})`],
3584
3816
  ["timezone", agent.timezone],
3585
3817
  ["machine", `${agent.machine.cpus} vCPU, ${agent.machine.memoryMb} MB`],
3586
3818
  ["docker", agent.machine.docker ? "yes" : "no"],
@@ -3605,6 +3837,272 @@ const detailsOf = (agent) => [
3605
3837
  ]))
3606
3838
  ].join("\n");
3607
3839
  //#endregion
3840
+ //#region src/commands/nodes.ts
3841
+ const nodeHints = {
3842
+ ...organizationHints,
3843
+ "node.not_found": "kcloud nodes list shows the nodes of the organization.",
3844
+ "node.name_taken": "Another node of the organization has that name.",
3845
+ "node.in_use": "Delete the agents placed on it first; kcloud nodes show <node> lists them.",
3846
+ "node.registration_used": "The node has joined and holds its own credential; it needs no token."
3847
+ };
3848
+ const nodesOf = async (api, organizationId) => dataOf("list the nodes", await api.GET("/organizations/{organizationId}/nodes", { params: { path: { organizationId } } }), nodeHints).items;
3849
+ const namesOf = (nodes) => nodes.map(({ name }) => name).join(", ");
3850
+ const nodeIn = (nodes, reference, organization) => {
3851
+ const found = nodes.find((node) => node.id === reference.toLowerCase() || node.name === reference);
3852
+ if (found === void 0) throw new CliError(`There is no node ${reference} in ${organization}.`, nodes.length === 0 ? "kcloud nodes create <name> creates one." : `kcloud nodes list shows ${namesOf(nodes)}.`);
3853
+ return found;
3854
+ };
3855
+ const resolveNode = async (scope, reference) => {
3856
+ const organization = await scope.organization();
3857
+ if (isId(reference)) return {
3858
+ organizationId: organization.id,
3859
+ nodeId: reference
3860
+ };
3861
+ const node = nodeIn(await nodesOf(scope.api, organization.id), reference, organization.slug);
3862
+ return {
3863
+ organizationId: organization.id,
3864
+ nodeId: node.id
3865
+ };
3866
+ };
3867
+ const nodeDetailOf = async (api, { organizationId, nodeId }) => dataOf("describe the node", await api.GET("/organizations/{organizationId}/nodes/{nodeId}", { params: { path: {
3868
+ organizationId,
3869
+ nodeId
3870
+ } } }), nodeHints);
3871
+ const createdNode = async (api, organizationId, name) => {
3872
+ const body = { name };
3873
+ validated(createNodeBodySchema, body, "a node");
3874
+ return dataOf("create the node", await api.POST("/organizations/{organizationId}/nodes", {
3875
+ params: { path: { organizationId } },
3876
+ body
3877
+ }), nodeHints);
3878
+ };
3879
+ const renewedRegistration = async (api, { organizationId, nodeId }) => dataOf("issue a registration token for the node", await api.POST("/organizations/{organizationId}/nodes/{nodeId}/registration", { params: { path: {
3880
+ organizationId,
3881
+ nodeId
3882
+ } } }), nodeHints);
3883
+ const deleteNode = async (api, { organizationId, nodeId }) => {
3884
+ doneWith("delete the node", await api.DELETE("/organizations/{organizationId}/nodes/{nodeId}", { params: { path: {
3885
+ organizationId,
3886
+ nodeId
3887
+ } } }), nodeHints);
3888
+ };
3889
+ const gibibytes = (bytes) => `${(bytes / 1024 ** 3).toFixed(1)} GiB`;
3890
+ const hostOf = (node) => node.host === null ? "-" : `${node.host.cpus} CPU, ${gibibytes(node.host.memoryBytes)} of memory`;
3891
+ const capabilitiesOf = (node) => {
3892
+ if (node.capabilities === null) return "-";
3893
+ const { sandbox, uidIsolation } = node.capabilities;
3894
+ return `sandbox ${sandbox ? "yes" : "no"}, uid isolation ${uidIsolation ? "yes" : "no"}`;
3895
+ };
3896
+ const registrationOf = (node) => node.registrationExpiresAt === null ? "-" : `a token waits until ${node.registrationExpiresAt}`;
3897
+ const storageOf = ({ state, freeBytes, totalBytes, path }) => `${state}, ${gibibytes(freeBytes)} free of ${gibibytes(totalBytes)} at ${path}`;
3898
+ const runtimeRowsOf = (runtime) => [
3899
+ ["reported", runtime.reportedAt],
3900
+ ["docker", runtime.docker.lastError === null ? runtime.docker.state : `${runtime.docker.state}: ${runtime.docker.lastError}`],
3901
+ ["data storage", storageOf(runtime.storage.data)],
3902
+ ["engine storage", storageOf(runtime.storage.engine)],
3903
+ ["last reconcile", runtime.reconcile.lastTickError === null ? runtime.reconcile.lastTickAt ?? "never" : `${runtime.reconcile.lastTickAt ?? "never"}: ${runtime.reconcile.lastTickError}`],
3904
+ ["agents", `${runtime.reconcile.agentsActive} active, ${runtime.reconcile.agentsQueued} queued`],
3905
+ ["monitor slots", `${runtime.sessions.monitor.inUse} of ${runtime.sessions.monitor.effective} in use`],
3906
+ ["executor slots", `${runtime.sessions.executor.inUse} of ${runtime.sessions.executor.effective} in use`],
3907
+ ["log readers", String(runtime.ingest.readers)]
3908
+ ];
3909
+ const nodeDetailsText = (node) => [
3910
+ renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
3911
+ ["name", node.name],
3912
+ ["state", node.state],
3913
+ ["provider", node.provider],
3914
+ ["engine", node.engine ?? "-"],
3915
+ ["version", node.version ?? "-"],
3916
+ ["host", hostOf(node)],
3917
+ ["server", node.host?.serverVersion ?? "-"],
3918
+ ["data root", node.host?.dataRoot ?? "-"],
3919
+ ["capabilities", capabilitiesOf(node)],
3920
+ ["registration", registrationOf(node)],
3921
+ ["connected", node.connectedAt ?? "never"],
3922
+ ["last seen", node.lastSeenAt ?? "never"],
3923
+ ["created", node.createdAt],
3924
+ ["id", node.id]
3925
+ ]),
3926
+ "",
3927
+ node.agents.length === 0 ? "No agent is placed on the node." : renderTable([
3928
+ { header: "PROJECT" },
3929
+ { header: "AGENT" },
3930
+ { header: "STATE" },
3931
+ { header: "ID" }
3932
+ ], node.agents.map((agent) => [
3933
+ agent.projectName,
3934
+ agent.slug,
3935
+ agent.state,
3936
+ agent.id
3937
+ ])),
3938
+ "",
3939
+ node.runtime === null ? "The node has reported no runtime yet." : renderTable([{ header: "RUNTIME" }, { header: "VALUE" }], runtimeRowsOf(node.runtime))
3940
+ ].join("\n");
3941
+ const startHintOf = (origin) => [
3942
+ "Start the node with it as docs/install.md of kikimora-cloud describes. On a Linux host, after read -rs token:",
3943
+ ` printf '%s\\n' "$token" | /opt/kikimora-cloud/infra/node/install.sh --platform-url ${origin} --image ghcr.io/kikimoradev/kikimora-cloud/node:<version> --registration-token -`,
3944
+ "On a Windows PC:",
3945
+ ` C:\\kikimora-cloud\\src\\infra\\windows\\install.ps1 -PlatformUrl ${origin} -RegistrationToken <token>`
3946
+ ].join("\n");
3947
+ const printedRegistration = (run, created) => {
3948
+ if (run.settings.json) {
3949
+ run.context.terminal.json(created);
3950
+ return exitCodes.ok;
3951
+ }
3952
+ run.context.terminal.line(created.registration.token);
3953
+ run.context.terminal.error([`The registration token of ${created.node.name} is shown only here and works once, until ${created.registration.expiresAt}.`, startHintOf(run.origin)].join("\n"));
3954
+ return exitCodes.ok;
3955
+ };
3956
+ const nodeUsage = [["", "a node is named by its name or its id"]];
3957
+ const createNodesCommand = (adaptersOf) => defineGroup({
3958
+ name: "nodes",
3959
+ summary: "The nodes of the organization, the hosts its agents run on.",
3960
+ usage: "nodes <command>",
3961
+ subcommands: [
3962
+ defineCommand({
3963
+ name: "list",
3964
+ summary: "List the nodes of the organization.",
3965
+ usage: "nodes list",
3966
+ options: {},
3967
+ positionals: {
3968
+ min: 0,
3969
+ max: 0
3970
+ },
3971
+ run: async (invocation) => {
3972
+ const run = await runOf(adaptersOf, invocation);
3973
+ const organization = await run.organization();
3974
+ const items = await nodesOf(run.api, organization.id);
3975
+ return printed(run, items, (nodes) => nodes.length === 0 ? `${organization.slug} has no node yet; kcloud nodes create <name> creates one.` : renderTable([
3976
+ { header: "NAME" },
3977
+ { header: "STATE" },
3978
+ { header: "ENGINE" },
3979
+ { header: "VERSION" },
3980
+ {
3981
+ header: "AGENTS",
3982
+ align: "right"
3983
+ },
3984
+ { header: "LAST SEEN" },
3985
+ { header: "ID" }
3986
+ ], nodes.map((node) => [
3987
+ node.name,
3988
+ node.state,
3989
+ node.engine ?? "-",
3990
+ node.version ?? "-",
3991
+ String(node.agentCount),
3992
+ node.lastSeenAt ?? "never",
3993
+ node.id
3994
+ ])));
3995
+ }
3996
+ }),
3997
+ defineCommand({
3998
+ name: "show",
3999
+ summary: "Show a node with its host, the agents placed on it and its runtime.",
4000
+ usage: "nodes show <node>",
4001
+ optionsUsage: nodeUsage,
4002
+ options: {},
4003
+ positionals: {
4004
+ min: 1,
4005
+ max: 1
4006
+ },
4007
+ run: async (invocation) => {
4008
+ const run = await runOf(adaptersOf, invocation);
4009
+ const reference = await resolveNode(run, invocation.positionals[0] ?? "");
4010
+ return printed(run, await nodeDetailOf(run.api, reference), nodeDetailsText);
4011
+ }
4012
+ }),
4013
+ defineCommand({
4014
+ name: "create",
4015
+ summary: "Create a node and print its one-time registration token.",
4016
+ usage: "nodes create <name>",
4017
+ optionsUsage: [["", "the name holds lowercase letters, digits and hyphens"], ["", "the token goes to stdout alone, how to start the node to stderr"]],
4018
+ options: {},
4019
+ positionals: {
4020
+ min: 1,
4021
+ max: 1
4022
+ },
4023
+ run: async (invocation) => {
4024
+ const run = await runOf(adaptersOf, invocation);
4025
+ const organization = await run.organization();
4026
+ return printedRegistration(run, await createdNode(run.api, organization.id, invocation.positionals[0] ?? ""));
4027
+ }
4028
+ }),
4029
+ defineCommand({
4030
+ name: "token",
4031
+ summary: "Issue a new registration token for a node that has not joined yet.",
4032
+ usage: "nodes token <node>",
4033
+ optionsUsage: [...nodeUsage, ["", "the token replaces the one issued before, which stops working"]],
4034
+ options: {},
4035
+ positionals: {
4036
+ min: 1,
4037
+ max: 1
4038
+ },
4039
+ run: async (invocation) => {
4040
+ const run = await runOf(adaptersOf, invocation);
4041
+ const reference = await resolveNode(run, invocation.positionals[0] ?? "");
4042
+ return printedRegistration(run, await renewedRegistration(run.api, reference));
4043
+ }
4044
+ }),
4045
+ defineCommand({
4046
+ name: "delete",
4047
+ summary: "Delete a node that runs no agent.",
4048
+ usage: "nodes delete <node>",
4049
+ optionsUsage: nodeUsage,
4050
+ options: {},
4051
+ positionals: {
4052
+ min: 1,
4053
+ max: 1
4054
+ },
4055
+ run: async (invocation) => {
4056
+ const run = await runOf(adaptersOf, invocation);
4057
+ const given = invocation.positionals[0] ?? "";
4058
+ const reference = await resolveNode(run, given);
4059
+ await confirmed({
4060
+ prompt: run.prompt,
4061
+ settings: run.settings,
4062
+ question: `Delete the node ${given}? Its host can no longer reach the platform.`
4063
+ });
4064
+ await deleteNode(run.api, reference);
4065
+ return said(run, `Deleted the node ${given}.`);
4066
+ }
4067
+ })
4068
+ ]
4069
+ });
4070
+ //#endregion
4071
+ //#region src/commands/projects/shared.ts
4072
+ const defaultProjectColor = "#7c86ff";
4073
+ const projectHint = {
4074
+ "project.not_found": "kcloud projects list shows the projects you are a member of.",
4075
+ "project.slug_taken": "Another project of the organization has that slug.",
4076
+ "project.invalid_slug": "A slug holds lowercase letters, digits and hyphens.",
4077
+ "project.not_empty": "Delete its agents, credentials, repositories and tools first.",
4078
+ "project.member_not_found": "kcloud projects members list <project> shows them.",
4079
+ "project.member_must_be_active": "kcloud orgs members enable <member> enables the account first."
4080
+ };
4081
+ const projectOf = async (api, id) => dataOf("describe the project", await api.GET("/projects/{id}", { params: { path: { id } } }), projectHint);
4082
+ //#endregion
4083
+ //#region src/commands/agents/placement.ts
4084
+ const placedNodeId = async (run, { projectId, given, ask }) => {
4085
+ const project = await projectOf(run.api, projectId);
4086
+ const organization = `the organization of ${project.slug}`;
4087
+ const nodes = await nodesOf(run.api, project.organizationId);
4088
+ if (given !== void 0) return nodeIn(nodes, given, organization).id;
4089
+ const [only, ...others] = nodes;
4090
+ if (only === void 0) throw new CliError(`The organization of ${project.slug} has no node to run the agent on.`, "kcloud nodes create <name> creates one, and its host joins with the token it prints.");
4091
+ if (others.length === 0) {
4092
+ run.context.terminal.error(`The agent runs on ${only.name}, the only node of ${organization}.`);
4093
+ return only.id;
4094
+ }
4095
+ if (ask) return run.prompt.select({
4096
+ message: "The node it runs on",
4097
+ choices: nodes.map((node) => ({
4098
+ value: node.id,
4099
+ label: node.name,
4100
+ hint: node.state
4101
+ }))
4102
+ });
4103
+ throw new UsageError(`agents create needs --node <name>, because ${organization} has ${nodes.length} nodes.`, `The nodes are ${nodes.map(({ name }) => name).join(", ")}.`);
4104
+ };
4105
+ //#endregion
3608
4106
  //#region src/commands/agents/create.ts
3609
4107
  const claudeKinds = /* @__PURE__ */ new Set(["anthropic_api_key", "claude_oauth_token"]);
3610
4108
  const catalogueOf = async (api, projectId) => {
@@ -3708,6 +4206,11 @@ const promptText = async (run, file, question) => {
3708
4206
  };
3709
4207
  const localTimezone = () => Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
3710
4208
  const askedPlan = async (run, projectId, values) => {
4209
+ const nodeId = await placedNodeId(run, {
4210
+ projectId,
4211
+ given: values.node,
4212
+ ask: !run.settings.yes
4213
+ });
3711
4214
  const catalogue = await catalogueOf(run.api, projectId);
3712
4215
  const name = values.name ?? await run.prompt.text({
3713
4216
  message: "The name of the agent",
@@ -3725,6 +4228,7 @@ const askedPlan = async (run, projectId, values) => {
3725
4228
  name,
3726
4229
  description: "",
3727
4230
  kikimoraVersion: release,
4231
+ nodeId,
3728
4232
  claudeCredentialId: await chosenCredential(run.prompt, catalogue, (kind) => claudeKinds.has(kind), "The Claude credential it works with"),
3729
4233
  githubCredentialId: await chosenCredential(run.prompt, catalogue, (kind) => kind === "github_pat", "The GitHub credential it clones with"),
3730
4234
  repositories: [...await chosenRepositories(run.prompt, catalogue)],
@@ -3743,17 +4247,27 @@ const askedPlan = async (run, projectId, values) => {
3743
4247
  }
3744
4248
  };
3745
4249
  };
3746
- const fileOf = async (run, file) => {
4250
+ const isObject = (document) => typeof document === "object" && document !== null && !Array.isArray(document);
4251
+ const fileOf = async (run, projectId, { file, node }) => {
3747
4252
  const document = await documentOf(run, file);
3748
- validated(createAgentBodySchema, document, "an agent");
3749
- return document;
4253
+ const placed = !isObject(document) || node === void 0 && "nodeId" in document ? document : {
4254
+ ...document,
4255
+ nodeId: await placedNodeId(run, {
4256
+ projectId,
4257
+ given: node,
4258
+ ask: false
4259
+ })
4260
+ };
4261
+ validated(createAgentBodySchema, placed, "an agent");
4262
+ return placed;
3750
4263
  };
3751
4264
  const createCommand = (adaptersOf) => defineCommand({
3752
4265
  name: "create",
3753
4266
  summary: "Create an agent from a file, from flags, or by asking step by step.",
3754
- usage: "agents create --project <name> [--file <path>] [--name <name>]",
4267
+ usage: "agents create --project <name> [--node <node>] [--file <path>] [--name <name>]",
3755
4268
  optionsUsage: [
3756
4269
  ["--project <name>", "the project that owns the agent"],
4270
+ ["--node <node>", "the node it runs on, by name or id; needed when the organization has several"],
3757
4271
  ["--file <path>", "the whole agent as JSON, or - to read stdin"],
3758
4272
  ["--name <name>", "the name of the agent, instead of asking"],
3759
4273
  ["--release <x.y.z>", "the Kikimora release it runs, instead of asking"],
@@ -3762,11 +4276,13 @@ const createCommand = (adaptersOf) => defineCommand({
3762
4276
  ["--timezone <zone>", "the timezone of the agent (default: this machine's)"],
3763
4277
  ["--docker", "give the agent a Docker sidecar, when the project allows it"],
3764
4278
  ["--browser", "give the agent a browser"],
3765
- ["", "kcloud agents show <agent> --as-file prints what --file takes"]
4279
+ ["", "kcloud agents show <agent> --as-file prints what --file takes"],
4280
+ ["", "--node replaces the nodeId of the file"]
3766
4281
  ],
3767
4282
  options: {
3768
4283
  ...fileOptions,
3769
4284
  project: { type: "string" },
4285
+ node: { type: "string" },
3770
4286
  name: { type: "string" },
3771
4287
  release: { type: "string" },
3772
4288
  "monitor-prompt-file": { type: "string" },
@@ -3789,8 +4305,11 @@ const createCommand = (adaptersOf) => defineCommand({
3789
4305
  const { values } = invocation;
3790
4306
  const run = await runOf(adaptersOf, invocation);
3791
4307
  if (values.project === void 0) throw new UsageError("agents create needs --project <name>.", "kcloud projects list shows the projects you are a member of.");
3792
- const projectId = await resolveProjectId(run.api, values.project);
3793
- const body = values.file === void 0 ? await askedPlan(run, projectId, values) : await fileOf(run, values.file);
4308
+ const projectId = await resolveProjectId(run, values.project);
4309
+ const body = values.file === void 0 ? await askedPlan(run, projectId, values) : await fileOf(run, projectId, {
4310
+ file: values.file,
4311
+ node: values.node
4312
+ });
3794
4313
  const agent = dataOf("create the agent", await run.api.POST("/projects/{id}/agents", {
3795
4314
  params: { path: { id: projectId } },
3796
4315
  body
@@ -3897,10 +4416,10 @@ const pageOptions = {
3897
4416
  };
3898
4417
  const allUsage = ["--all", "follow the cursor to the end instead of one page"];
3899
4418
  const pageUsage = [["--limit <n>", `how many rows (default: 50)`], allUsage];
3900
- const dayOf = (date) => date.toISOString().slice(0, 10);
4419
+ const dayOf$1 = (date) => date.toISOString().slice(0, 10);
3901
4420
  const costWindowOf = (days, now) => ({
3902
- from: dayOf(/* @__PURE__ */ new Date(now.getTime() - (days - 1) * 24 * 60 * 6e4)),
3903
- to: dayOf(now)
4421
+ from: dayOf$1(/* @__PURE__ */ new Date(now.getTime() - (days - 1) * 24 * 60 * 6e4)),
4422
+ to: dayOf$1(now)
3904
4423
  });
3905
4424
  const windowFrom = (days, now) => {
3906
4425
  const span = days === void 0 ? 7 : Number(days);
@@ -4334,17 +4853,6 @@ const streamFrames = async (request) => {
4334
4853
  }
4335
4854
  };
4336
4855
  //#endregion
4337
- //#region src/profile/token.ts
4338
- const tokenOf = async ({ settings, context }) => {
4339
- const fromEnvironment = context.environment[tokenVariable];
4340
- if (fromEnvironment !== void 0 && fromEnvironment !== "") return fromEnvironment;
4341
- const store = await readProfileStore(context.homeDirectory);
4342
- const name = settings.profile ?? store.current;
4343
- const profile = name === null ? void 0 : store.profiles[name];
4344
- if (profile === void 0) throw new CliError("No instance is signed in here.", "Run kcloud login --url <origin> first.");
4345
- return profile.token;
4346
- };
4347
- //#endregion
4348
4856
  //#region src/commands/agents/streams.ts
4349
4857
  const streamOptions = {
4350
4858
  ...agentOption,
@@ -4397,15 +4905,12 @@ const streamCommands = (adaptersOf) => [{
4397
4905
  api: adapters.api
4398
4906
  });
4399
4907
  const reference = positionals[0] ?? "";
4400
- const agentId = await resolveAgentId(session.api, reference, values.project);
4908
+ const agentId = await resolveAgentId(session, reference, values.project);
4401
4909
  const limit = values.limit;
4402
4910
  if (limit !== void 0 && !/^[0-9]+$/u.test(limit)) throw new UsageError(`--limit ${limit} is not a whole number.`);
4403
4911
  await streamFrames({
4404
4912
  origin: session.origin,
4405
- token: await tokenOf({
4406
- settings,
4407
- context
4408
- }),
4913
+ token: session.token,
4409
4914
  path: path.replace("{id}", encodeURIComponent(agentId)),
4410
4915
  query: {
4411
4916
  ...values.since === void 0 ? {} : { since: values.since },
@@ -4601,7 +5106,7 @@ const pipedText = async (stream) => {
4601
5106
  for await (const chunk of stream) chunks.push(Buffer.from(chunk));
4602
5107
  return Buffer.concat(chunks).toString("utf8");
4603
5108
  };
4604
- const secretOf = async ({ run, file, fromStdin = false, question, variable = secretVariable }) => {
5109
+ const secretOf = async ({ run, file, fromStdin = false, question, variable = secretVariable, stdinHint = "Pipe the secret in, or drop --secret-stdin to be asked for it." }) => {
4605
5110
  if (file !== void 0) {
4606
5111
  const path = resolve(run.context.workingDirectory, file);
4607
5112
  try {
@@ -4611,7 +5116,7 @@ const secretOf = async ({ run, file, fromStdin = false, question, variable = sec
4611
5116
  }
4612
5117
  }
4613
5118
  if (fromStdin) {
4614
- if (isTerminal(run.context.io.stdin)) throw new UsageError("Nothing is piped on stdin.", "Pipe the secret in, or drop --secret-stdin to be asked for it.");
5119
+ if (isTerminal(run.context.io.stdin)) throw new UsageError("Nothing is piped on stdin.", stdinHint);
4615
5120
  return (await pipedText(run.context.io.stdin)).trim();
4616
5121
  }
4617
5122
  const fromEnvironment = run.context.environment[variable];
@@ -4646,7 +5151,7 @@ const credentialHints = {
4646
5151
  };
4647
5152
  const projectFrom$2 = async (run, given) => {
4648
5153
  if (given === void 0) throw new UsageError("This command needs a project.", "Pass --project <name>; kcloud projects list shows them.");
4649
- return resolveProjectId(run.api, given);
5154
+ return resolveProjectId(run, given);
4650
5155
  };
4651
5156
  const credentialsOf = async (api, projectId) => dataOf("list the credentials of the project", await api.GET("/projects/{id}/credentials", { params: { path: { id: projectId } } }), credentialHints).items;
4652
5157
  const resolveCredentialId = async (api, projectId, reference) => {
@@ -5020,17 +5525,18 @@ const accountTools = [
5020
5525
  defineTool({
5021
5526
  name: "whoami",
5022
5527
  title: "Who am I",
5023
- description: "Say which instance and which account this server acts as, and whether the version of kcloud matches the version of the instance.",
5528
+ description: "Say which instance, account and organization this server acts with, and whether the version of kcloud matches the version of the instance.",
5024
5529
  operations: ["getMe", "getPlatformVersion"],
5025
5530
  safety: "read",
5026
5531
  input: z.object({}),
5027
5532
  run: async (_, run) => {
5028
- const me = dataOf("say who the token belongs to", await run.api.GET("/auth/me"));
5533
+ const user = await run.me();
5534
+ const actsIn = await run.organization();
5029
5535
  const platform = dataOf("say which version it runs", await run.api.GET("/version"));
5030
5536
  return {
5031
5537
  origin: run.origin,
5032
- profile: run.profileName,
5033
- user: me,
5538
+ user,
5539
+ actsIn,
5034
5540
  kcloud: run.kcloudVersion,
5035
5541
  instance: platform.version,
5036
5542
  matched: platform.version === run.kcloudVersion
@@ -5084,8 +5590,8 @@ const windowOfPage = ({ limit, all }, fallback = 50) => ({
5084
5590
  limit: limit ?? fallback,
5085
5591
  all
5086
5592
  });
5087
- const projectIdOf = (run, project) => resolveProjectId(run.api, project);
5088
- const agentIdOf = (run, { agent, project }) => resolveAgentId(run.api, agent, project);
5593
+ const projectIdOf = (run, project) => resolveProjectId(run, project);
5594
+ const agentIdOf = (run, { agent, project }) => resolveAgentId(run, agent, project);
5089
5595
  //#endregion
5090
5596
  //#region src/mcp/tools/agents.ts
5091
5597
  const actionOn = async (run, agentId, action, force) => dataOf(`${action} the agent`, await run.api.POST("/agents/{id}/actions/{action}", {
@@ -5103,7 +5609,7 @@ const agentTools = [
5103
5609
  operations: ["listAgents"],
5104
5610
  safety: "read",
5105
5611
  input: z.object({ project: projectField.optional().describe("One project instead of all of them.") }),
5106
- run: async ({ project }, run) => agentsOf(run.api, project)
5612
+ run: async ({ project }, run) => agentsOf(run, project)
5107
5613
  }),
5108
5614
  defineTool({
5109
5615
  name: "agent_show",
@@ -5123,7 +5629,7 @@ const agentTools = [
5123
5629
  defineTool({
5124
5630
  name: "agent_create",
5125
5631
  title: "Create an agent",
5126
- description: "Create an agent in a project. The credentials, repositories and tools are named by the ids that credentials_list, repositories_list and tools_list return; agent_show with asPlan shows a complete example. The agent stays stopped until agent_action starts it.",
5632
+ description: "Create an agent in a project. The node it runs on, the credentials, repositories and tools are named by the ids that nodes_list, credentials_list, repositories_list and tools_list return; agent_show with asPlan shows a complete example. The agent stays stopped until agent_action starts it.",
5127
5633
  operations: ["createAgent"],
5128
5634
  safety: "write",
5129
5635
  input: z.object({
@@ -5288,11 +5794,47 @@ const credentialTools = [
5288
5794
  })
5289
5795
  ];
5290
5796
  //#endregion
5291
- //#region src/commands/notifications.ts
5292
- const notificationHints = { "notification.not_found": "kcloud notifications list shows them." };
5293
- const createNotificationsCommand = (adaptersOf) => defineGroup({
5294
- name: "notifications",
5295
- summary: "What the instance wants you to look at.",
5797
+ //#region src/mcp/tools/nodes.ts
5798
+ const nodeField = z.string().trim().min(1).describe("The node, by its name or its id, from nodes_list.");
5799
+ const nodeTools = [
5800
+ defineTool({
5801
+ name: "nodes_list",
5802
+ title: "List nodes",
5803
+ description: "List the nodes of the organization this server acts in, the hosts its agents run on, with their state, engine, version and agent count.",
5804
+ operations: ["listNodes"],
5805
+ safety: "read",
5806
+ input: z.object({}),
5807
+ run: async (_, run) => nodesOf(run.api, (await run.organization()).id)
5808
+ }),
5809
+ defineTool({
5810
+ name: "node_show",
5811
+ title: "Show a node",
5812
+ description: "Show one node with its host, its capabilities, the agents placed on it and the runtime it reported last.",
5813
+ operations: ["getNode"],
5814
+ safety: "read",
5815
+ input: z.object({ node: nodeField }),
5816
+ run: async ({ node }, run) => nodeDetailOf(run.api, await resolveNode(run, node))
5817
+ }),
5818
+ defineTool({
5819
+ name: "node_delete",
5820
+ title: "Delete a node",
5821
+ description: "Delete a node, after which its host can no longer reach the platform. The instance refuses while agents are placed on it.",
5822
+ operations: ["deleteNode"],
5823
+ safety: "destructive",
5824
+ input: z.object({ node: nodeField }),
5825
+ run: async ({ node }, run) => {
5826
+ const reference = await resolveNode(run, node);
5827
+ await deleteNode(run.api, reference);
5828
+ return { deleted: reference.nodeId };
5829
+ }
5830
+ })
5831
+ ];
5832
+ //#endregion
5833
+ //#region src/commands/notifications.ts
5834
+ const notificationHints = { "notification.not_found": "kcloud notifications list shows them." };
5835
+ const createNotificationsCommand = (adaptersOf) => defineGroup({
5836
+ name: "notifications",
5837
+ summary: "What the instance wants you to look at.",
5296
5838
  usage: "notifications <command>",
5297
5839
  subcommands: [
5298
5840
  defineCommand({
@@ -5391,10 +5933,7 @@ const createNotificationsCommand = (adaptersOf) => defineGroup({
5391
5933
  });
5392
5934
  await streamFrames({
5393
5935
  origin: session.origin,
5394
- token: await tokenOf({
5395
- settings,
5396
- context
5397
- }),
5936
+ token: session.token,
5398
5937
  path: "/notifications/stream",
5399
5938
  follow: true,
5400
5939
  action: "follow the notifications",
@@ -5649,6 +6188,534 @@ const observabilityTools = [
5649
6188
  })
5650
6189
  ];
5651
6190
  //#endregion
6191
+ //#region src/commands/roles.ts
6192
+ const roleOption = { role: { type: "string" } };
6193
+ const roleUsage = ["--role <role>", "admin or member"];
6194
+ const roleFrom = (given, command) => {
6195
+ if (given === void 0) throw new UsageError(`${command} needs --role admin or --role member.`);
6196
+ const parsed = organizationRoleSchema.safeParse(given);
6197
+ if (!parsed.success) throw new UsageError(`--role ${given} is not a role.`, "The roles are admin and member.");
6198
+ return parsed.data;
6199
+ };
6200
+ const asRole = (role) => role === "admin" ? "an admin" : "a member";
6201
+ //#endregion
6202
+ //#region src/commands/orgs/members.ts
6203
+ const memberHints = {
6204
+ ...organizationHints,
6205
+ "user.not_found": "kcloud orgs members list shows them.",
6206
+ "user.email_taken": "An account with that e-mail exists already.",
6207
+ "user.cannot_modify_self": "Another admin of the organization has to make that change.",
6208
+ "user.last_admin": "Make another member an admin first.",
6209
+ "auth.forbidden": "Changing the members of an organization takes an admin of it."
6210
+ };
6211
+ const passwordLength = `between ${passwordSchema.minLength ?? 0} and ${passwordSchema.maxLength ?? 0} characters`;
6212
+ const memberPath = (organizationId, userId) => ({ params: { path: {
6213
+ organizationId,
6214
+ userId
6215
+ } } });
6216
+ const memberRoleChanged = async (api, organizationId, userId, role) => dataOf("change the role of the member", await api.PATCH("/organizations/{organizationId}/members/{userId}", {
6217
+ ...memberPath(organizationId, userId),
6218
+ body: { organizationRole: role }
6219
+ }), memberHints);
6220
+ const memberDisabled = async (api, organizationId, userId) => dataOf("disable the member", await api.POST("/organizations/{organizationId}/members/{userId}/disable", memberPath(organizationId, userId)), memberHints);
6221
+ const memberEnabled = async (api, organizationId, userId) => dataOf("enable the member", await api.POST("/organizations/{organizationId}/members/{userId}/enable", memberPath(organizationId, userId)), memberHints);
6222
+ const passwordReset = async (api, organizationId, userId) => dataOf("reset the password of the member", await api.POST("/organizations/{organizationId}/members/{userId}/reset-password", memberPath(organizationId, userId)), memberHints);
6223
+ const chosenMember$1 = async (run, reference) => {
6224
+ const organization = await run.organization();
6225
+ return {
6226
+ organization,
6227
+ member: await resolveOrganizationMember(run.api, organization.id, reference)
6228
+ };
6229
+ };
6230
+ const localeFrom = (given) => {
6231
+ if (given === void 0) return "en";
6232
+ const parsed = localeSchema.safeParse(given);
6233
+ if (!parsed.success) throw new UsageError(`--locale ${given} is not a language of the panel.`, "The languages are en and pl.");
6234
+ return parsed.data;
6235
+ };
6236
+ const initialPasswordOf = async (run, source) => {
6237
+ const file = source["initial-password-file"];
6238
+ const fromStdin = source["initial-password-stdin"];
6239
+ if (file === void 0 && !fromStdin) return null;
6240
+ const password = await secretOf({
6241
+ run,
6242
+ file,
6243
+ fromStdin,
6244
+ question: "The initial password",
6245
+ stdinHint: "Pipe the password in, or drop --initial-password-stdin."
6246
+ });
6247
+ if (!passwordSchema.safeParse(password).success) throw new UsageError(`The initial password is not ${passwordLength}.`);
6248
+ return password;
6249
+ };
6250
+ const printedPassword = (run, password, note) => {
6251
+ run.context.terminal.line(password);
6252
+ run.context.terminal.error(note);
6253
+ return exitCodes.ok;
6254
+ };
6255
+ const memberUsage = [["", "the member is named by e-mail or id"]];
6256
+ const organizationMembersCommand = (adaptersOf) => defineGroup({
6257
+ name: "members",
6258
+ summary: "The people of the organization kcloud acts in.",
6259
+ usage: "orgs members <command>",
6260
+ subcommands: [
6261
+ defineCommand({
6262
+ name: "list",
6263
+ summary: "List the members of the organization.",
6264
+ usage: "orgs members list",
6265
+ options: {},
6266
+ positionals: {
6267
+ min: 0,
6268
+ max: 0
6269
+ },
6270
+ run: async (invocation) => {
6271
+ const run = await runOf(adaptersOf, invocation);
6272
+ const { id } = await run.organization();
6273
+ const items = await organizationMembersOf(run.api, id);
6274
+ return printed(run, items, (members) => renderTable([
6275
+ { header: "EMAIL" },
6276
+ { header: "ROLE" },
6277
+ { header: "STATUS" },
6278
+ { header: "LAST SIGN-IN" },
6279
+ { header: "ID" }
6280
+ ], members.map((member) => [
6281
+ member.email,
6282
+ member.platformAdmin ? `${member.organizationRole}, platform admin` : member.organizationRole,
6283
+ member.status,
6284
+ member.lastLoginAt ?? "never",
6285
+ member.id
6286
+ ])));
6287
+ }
6288
+ }),
6289
+ defineCommand({
6290
+ name: "add",
6291
+ summary: "Add a person to the organization, printing a temporary password once.",
6292
+ usage: "orgs members add --email <email> --role <role> [--locale <locale>] [--initial-password-file <path> | --initial-password-stdin]",
6293
+ optionsUsage: [
6294
+ ["--email <email>", "the e-mail the person signs in with"],
6295
+ roleUsage,
6296
+ ["--locale <locale>", "the language of the panel for them, en or pl (default: en)"],
6297
+ ["--initial-password-file <path>", "read the first password from a file"],
6298
+ ["--initial-password-stdin", "read the first password from stdin"],
6299
+ ["", "a password is never taken from a flag"],
6300
+ ["", "without one the instance makes a temporary password and kcloud prints it"]
6301
+ ],
6302
+ options: {
6303
+ email: { type: "string" },
6304
+ ...roleOption,
6305
+ locale: { type: "string" },
6306
+ "initial-password-file": { type: "string" },
6307
+ "initial-password-stdin": {
6308
+ type: "boolean",
6309
+ default: false
6310
+ }
6311
+ },
6312
+ positionals: {
6313
+ min: 0,
6314
+ max: 0
6315
+ },
6316
+ run: async (invocation) => {
6317
+ const { values } = invocation;
6318
+ const email = values.email;
6319
+ if (email === void 0) throw new UsageError("orgs members add needs --email <email>.");
6320
+ const role = roleFrom(values.role, "orgs members add");
6321
+ const locale = localeFrom(values.locale);
6322
+ if (values["initial-password-file"] !== void 0 && values["initial-password-stdin"]) throw new UsageError("Pass --initial-password-file or --initial-password-stdin, not both.");
6323
+ const run = await runOf(adaptersOf, invocation);
6324
+ const initialPassword = await initialPasswordOf(run, values);
6325
+ const body = {
6326
+ email,
6327
+ organizationRole: role,
6328
+ locale,
6329
+ ...initialPassword === null ? {} : { initialPassword }
6330
+ };
6331
+ validated(createUserBodySchema, body, "a new member");
6332
+ const organization = await run.organization();
6333
+ const created = dataOf("add the member", await run.api.POST("/organizations/{organizationId}/members", {
6334
+ params: { path: { organizationId: organization.id } },
6335
+ body
6336
+ }), memberHints);
6337
+ if (run.settings.json) {
6338
+ run.context.terminal.json(created);
6339
+ return exitCodes.ok;
6340
+ }
6341
+ const added = `Added ${created.user.email} to ${organization.slug} as ${asRole(role)}.`;
6342
+ return created.temporaryPassword === null ? said(run, added) : printedPassword(run, created.temporaryPassword, `${added} The temporary password is shown only here.`);
6343
+ }
6344
+ }),
6345
+ defineCommand({
6346
+ name: "set-role",
6347
+ summary: "Make a member an admin of the organization, or a member again.",
6348
+ usage: "orgs members set-role <member> --role <role>",
6349
+ optionsUsage: [roleUsage, ...memberUsage],
6350
+ options: roleOption,
6351
+ positionals: {
6352
+ min: 1,
6353
+ max: 1
6354
+ },
6355
+ run: async (invocation) => {
6356
+ const role = roleFrom(invocation.values.role, "orgs members set-role");
6357
+ const run = await runOf(adaptersOf, invocation);
6358
+ const { organization, member } = await chosenMember$1(run, invocation.positionals[0] ?? "");
6359
+ const user = await memberRoleChanged(run.api, organization.id, member.id, role);
6360
+ return printed(run, user, (shown) => `${shown.email} is ${asRole(shown.organizationRole)} of ${organization.slug} now.`);
6361
+ }
6362
+ }),
6363
+ defineCommand({
6364
+ name: "disable",
6365
+ summary: "Stop a member from signing in, keeping the account.",
6366
+ usage: "orgs members disable <member>",
6367
+ optionsUsage: memberUsage,
6368
+ options: {},
6369
+ positionals: {
6370
+ min: 1,
6371
+ max: 1
6372
+ },
6373
+ run: async (invocation) => {
6374
+ const run = await runOf(adaptersOf, invocation);
6375
+ const { organization, member } = await chosenMember$1(run, invocation.positionals[0] ?? "");
6376
+ await confirmed({
6377
+ prompt: run.prompt,
6378
+ settings: run.settings,
6379
+ question: `Disable ${member.email}? The account cannot sign in until it is enabled again.`
6380
+ });
6381
+ const user = await memberDisabled(run.api, organization.id, member.id);
6382
+ return printed(run, user, (shown) => `Disabled ${shown.email}.`);
6383
+ }
6384
+ }),
6385
+ defineCommand({
6386
+ name: "enable",
6387
+ summary: "Let a disabled member sign in again.",
6388
+ usage: "orgs members enable <member>",
6389
+ optionsUsage: memberUsage,
6390
+ options: {},
6391
+ positionals: {
6392
+ min: 1,
6393
+ max: 1
6394
+ },
6395
+ run: async (invocation) => {
6396
+ const run = await runOf(adaptersOf, invocation);
6397
+ const { organization, member } = await chosenMember$1(run, invocation.positionals[0] ?? "");
6398
+ const user = await memberEnabled(run.api, organization.id, member.id);
6399
+ return printed(run, user, (shown) => `Enabled ${shown.email}.`);
6400
+ }
6401
+ }),
6402
+ defineCommand({
6403
+ name: "reset-password",
6404
+ summary: "Give a member a new temporary password and print it once.",
6405
+ usage: "orgs members reset-password <member>",
6406
+ optionsUsage: memberUsage,
6407
+ options: {},
6408
+ positionals: {
6409
+ min: 1,
6410
+ max: 1
6411
+ },
6412
+ run: async (invocation) => {
6413
+ const run = await runOf(adaptersOf, invocation);
6414
+ const { organization, member } = await chosenMember$1(run, invocation.positionals[0] ?? "");
6415
+ await confirmed({
6416
+ prompt: run.prompt,
6417
+ settings: run.settings,
6418
+ question: `Reset the password of ${member.email}? The current one stops working.`
6419
+ });
6420
+ const reset = await passwordReset(run.api, organization.id, member.id);
6421
+ if (run.settings.json) {
6422
+ run.context.terminal.json(reset);
6423
+ return exitCodes.ok;
6424
+ }
6425
+ return printedPassword(run, reset.temporaryPassword, `The temporary password of ${member.email} is shown only here.`);
6426
+ }
6427
+ })
6428
+ ]
6429
+ });
6430
+ //#endregion
6431
+ //#region src/commands/orgs/index.ts
6432
+ const organizationOrInUse = async (scope, reference) => reference === void 0 ? scope.organization() : organizationIn(await scope.organizations(), reference);
6433
+ const organizationsMarked = async (scope, warn) => {
6434
+ const organizations = await scope.organizations();
6435
+ const inUse = await scope.organization().then(({ id }) => id, (error) => {
6436
+ if (!(error instanceof CliError)) throw error;
6437
+ warn(error);
6438
+ return null;
6439
+ });
6440
+ return organizations.map((organization) => ({
6441
+ ...organization,
6442
+ inUse: organization.id === inUse
6443
+ }));
6444
+ };
6445
+ const organizationCostsOf = async (api, organizationId, { from, to }) => dataOf("show what the organization spent", await api.GET("/organizations/{organizationId}/costs", { params: {
6446
+ path: { organizationId },
6447
+ query: {
6448
+ from,
6449
+ to
6450
+ }
6451
+ } }), organizationHints);
6452
+ const dayOf = (flag, given) => {
6453
+ if (!z.iso.date().safeParse(given).success) throw new UsageError(`--${flag} ${given} is not a day written as YYYY-MM-DD.`);
6454
+ return given;
6455
+ };
6456
+ const shiftedDay = (day, days) => new Date(Date.parse(`${day}T00:00:00.000Z`) + days * 24 * 60 * 6e4).toISOString().slice(0, 10);
6457
+ const costWindowFrom = (from, to, now) => {
6458
+ const last = to === void 0 ? now.toISOString().slice(0, 10) : dayOf("to", to);
6459
+ const first = from === void 0 ? shiftedDay(last, -6) : dayOf("from", from);
6460
+ if (first > last) throw new UsageError(`--from ${first} comes after --to ${last}.`);
6461
+ return {
6462
+ from: first,
6463
+ to: last
6464
+ };
6465
+ };
6466
+ const costsText = (costs, { from, to }, slugs) => [
6467
+ costs.items.length === 0 ? `The organization spent nothing between ${from} and ${to}.` : renderTable([
6468
+ { header: "DAY" },
6469
+ {
6470
+ header: "COST",
6471
+ align: "right"
6472
+ },
6473
+ {
6474
+ header: "RUNS",
6475
+ align: "right"
6476
+ }
6477
+ ], costs.items.map((point) => [
6478
+ point.day,
6479
+ point.costUsd.toFixed(2),
6480
+ String(point.runs)
6481
+ ])),
6482
+ ...costs.projects.length === 0 ? [] : ["", renderTable([
6483
+ { header: "PROJECT" },
6484
+ {
6485
+ header: "COST",
6486
+ align: "right"
6487
+ },
6488
+ {
6489
+ header: "RUNS",
6490
+ align: "right"
6491
+ }
6492
+ ], costs.projects.map((project) => [
6493
+ slugs.get(project.projectId) ?? project.projectId,
6494
+ project.costUsd.toFixed(2),
6495
+ String(project.runs)
6496
+ ]))],
6497
+ "",
6498
+ `${costs.totalCostUsd.toFixed(2)} over ${costs.totalRuns} runs and ${costs.sessions} sessions.`
6499
+ ].join("\n");
6500
+ const createOrgsCommand = (adaptersOf) => defineGroup({
6501
+ name: "orgs",
6502
+ summary: "The organizations you can see, and the one kcloud acts in.",
6503
+ usage: "orgs <command>",
6504
+ subcommands: [
6505
+ defineCommand({
6506
+ name: "list",
6507
+ summary: "List the organizations you can see, marking the one kcloud acts in.",
6508
+ usage: "orgs list",
6509
+ options: {},
6510
+ positionals: {
6511
+ min: 0,
6512
+ max: 0
6513
+ },
6514
+ run: async (invocation) => {
6515
+ const run = await runOf(adaptersOf, invocation);
6516
+ const items = await organizationsMarked(run, (problem) => {
6517
+ run.context.terminal.error(run.context.terminal.errorStyles.warning(`warning ${problem.message}`));
6518
+ });
6519
+ return printed(run, items, (listed) => listed.length === 0 ? "You can see no organization." : renderTable([
6520
+ { header: "SLUG" },
6521
+ { header: "NAME" },
6522
+ { header: "ID" }
6523
+ ], listed.map((organization) => [
6524
+ organization.inUse ? `${organization.slug} *` : organization.slug,
6525
+ organization.name,
6526
+ organization.id
6527
+ ])));
6528
+ }
6529
+ }),
6530
+ defineCommand({
6531
+ name: "use",
6532
+ summary: "Act in an organization on this instance from now on.",
6533
+ usage: "orgs use <organization> | orgs use --clear",
6534
+ optionsUsage: [["--clear", "act in your own organization again"], ["", `--org and ${organizationVariable} still win over the one stored here`]],
6535
+ options: { clear: {
6536
+ type: "boolean",
6537
+ default: false
6538
+ } },
6539
+ positionals: {
6540
+ min: 0,
6541
+ max: 1
6542
+ },
6543
+ run: async ({ values, positionals, settings, context }) => {
6544
+ const [reference] = positionals;
6545
+ if (values.clear === (reference !== void 0)) throw new UsageError("orgs use needs an organization or --clear, and not both.", "kcloud orgs list shows the organizations you can see.");
6546
+ const adapters = adaptersOf(context);
6547
+ const session = await openSession({
6548
+ settings,
6549
+ context,
6550
+ api: adapters.api
6551
+ });
6552
+ if (session.stored === null) throw new CliError(`${tokenVariable} signs in to ${session.origin}, so nothing is stored here to keep the organization with.`, `Pass --org or set ${organizationVariable} instead.`);
6553
+ const organization = reference === void 0 ? null : organizationIn(await session.organizations(), reference);
6554
+ await writeInstanceStore(context.homeDirectory, withInstance(session.store, session.origin, {
6555
+ ...session.stored,
6556
+ organization: organization?.slug ?? null
6557
+ }));
6558
+ if (givenIn(context.environment, "KIKIMORA_CLOUD_ORG") !== null) context.terminal.error(context.terminal.errorStyles.warning(`warning ${organizationVariable} still wins over it in this environment.`));
6559
+ return printed({
6560
+ settings,
6561
+ context
6562
+ }, {
6563
+ origin: session.origin,
6564
+ organization
6565
+ }, ({ origin }) => organization === null ? `kcloud acts in your own organization on ${origin} again.` : `kcloud acts in ${organizationNameOf(organization)} on ${origin} from now on.`);
6566
+ }
6567
+ }),
6568
+ defineCommand({
6569
+ name: "show",
6570
+ summary: "Show an organization, by default the one kcloud acts in.",
6571
+ usage: "orgs show [<organization>]",
6572
+ options: {},
6573
+ positionals: {
6574
+ min: 0,
6575
+ max: 1
6576
+ },
6577
+ run: async (invocation) => {
6578
+ const run = await runOf(adaptersOf, invocation);
6579
+ const { id } = await organizationOrInUse(run, invocation.positionals[0]);
6580
+ const organization = dataOf("describe the organization", await run.api.GET("/organizations/{organizationId}", { params: { path: { organizationId: id } } }), organizationHints);
6581
+ return printed(run, organization, (shown) => renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
6582
+ ["name", shown.name],
6583
+ ["slug", shown.slug],
6584
+ ["created", shown.createdAt],
6585
+ ["id", shown.id]
6586
+ ]));
6587
+ }
6588
+ }),
6589
+ defineCommand({
6590
+ name: "costs",
6591
+ summary: "Show what the organization spent, by day and by project.",
6592
+ usage: "orgs costs [--from <day>] [--to <day>]",
6593
+ optionsUsage: [["--from <day>", `the first day, as YYYY-MM-DD (default: 7 days up to --to)`], ["--to <day>", "the last day, as YYYY-MM-DD (default: today)"]],
6594
+ options: {
6595
+ from: { type: "string" },
6596
+ to: { type: "string" }
6597
+ },
6598
+ positionals: {
6599
+ min: 0,
6600
+ max: 0
6601
+ },
6602
+ run: async (invocation) => {
6603
+ const { values } = invocation;
6604
+ const run = await runOf(adaptersOf, invocation);
6605
+ const window = costWindowFrom(values.from, values.to, run.adapters.now());
6606
+ const organization = await run.organization();
6607
+ const costs = await organizationCostsOf(run.api, organization.id, window);
6608
+ if (run.settings.json) {
6609
+ run.context.terminal.json(costs);
6610
+ return exitCodes.ok;
6611
+ }
6612
+ const slugs = new Map(costs.projects.length === 0 ? [] : (await projectsOf(run)).map(({ id, slug }) => [id, slug]));
6613
+ run.context.terminal.line(costsText(costs, window, slugs));
6614
+ return exitCodes.ok;
6615
+ }
6616
+ }),
6617
+ organizationMembersCommand(adaptersOf)
6618
+ ]
6619
+ });
6620
+ //#endregion
6621
+ //#region src/mcp/tools/organizations.ts
6622
+ const memberField = z.string().trim().min(1).describe("The member, by e-mail or id, from organization_members_list.");
6623
+ const dayField = z.iso.date();
6624
+ const chosenMember = async (run, member) => {
6625
+ const { id } = await run.organization();
6626
+ return {
6627
+ organizationId: id,
6628
+ userId: (await resolveOrganizationMember(run.api, id, member)).id
6629
+ };
6630
+ };
6631
+ const organizationTools = [
6632
+ defineTool({
6633
+ name: "organizations_list",
6634
+ title: "List organizations",
6635
+ description: "List the organizations you can see, marking the one this server acts in. A member sees one, a platform admin sees all.",
6636
+ operations: ["listOrganizations"],
6637
+ safety: "read",
6638
+ input: z.object({}),
6639
+ run: (_, run) => organizationsMarked(run, () => void 0)
6640
+ }),
6641
+ defineTool({
6642
+ name: "organization_show",
6643
+ title: "Show an organization",
6644
+ description: "Show an organization, by default the one this server acts in.",
6645
+ operations: ["getOrganization"],
6646
+ safety: "read",
6647
+ input: z.object({ organization: z.string().trim().min(1).optional().describe("The organization, by its slug, its name or its id.") }),
6648
+ run: async ({ organization }, run) => {
6649
+ const { id } = await organizationOrInUse(run, organization);
6650
+ return dataOf("describe the organization", await run.api.GET("/organizations/{organizationId}", { params: { path: { organizationId: id } } }), organizationHints);
6651
+ }
6652
+ }),
6653
+ defineTool({
6654
+ name: "organization_costs",
6655
+ title: "Show what the organization spent",
6656
+ description: "Show what the organization this server acts in spent, by day and by project, over the last seven days unless from and to say otherwise.",
6657
+ operations: ["getOrganizationCosts"],
6658
+ safety: "read",
6659
+ input: z.object({
6660
+ from: dayField.optional().describe("The first day, as YYYY-MM-DD."),
6661
+ to: dayField.optional().describe("The last day, as YYYY-MM-DD, today by default.")
6662
+ }),
6663
+ run: async ({ from, to }, run) => {
6664
+ const { id } = await run.organization();
6665
+ return organizationCostsOf(run.api, id, costWindowFrom(from, to, run.now()));
6666
+ }
6667
+ }),
6668
+ defineTool({
6669
+ name: "organization_members_list",
6670
+ title: "List the members of the organization",
6671
+ description: "List the people of the organization this server acts in, with their role and status.",
6672
+ operations: ["listOrganizationMembers"],
6673
+ safety: "read",
6674
+ input: z.object({}),
6675
+ run: async (_, run) => organizationMembersOf(run.api, (await run.organization()).id)
6676
+ }),
6677
+ defineTool({
6678
+ name: "organization_member_set_role",
6679
+ title: "Set the role of a member",
6680
+ description: "Make a member an admin of the organization, or a member again. It takes an admin of the organization.",
6681
+ operations: ["updateOrganizationMember"],
6682
+ safety: "write",
6683
+ idempotent: true,
6684
+ input: z.object({
6685
+ member: memberField,
6686
+ role: organizationRoleSchema
6687
+ }),
6688
+ run: async ({ member, role }, run) => {
6689
+ const { organizationId, userId } = await chosenMember(run, member);
6690
+ return memberRoleChanged(run.api, organizationId, userId, role);
6691
+ }
6692
+ }),
6693
+ defineTool({
6694
+ name: "organization_member_disable",
6695
+ title: "Disable a member",
6696
+ description: "Stop a member from signing in, keeping the account. It takes an admin of the organization.",
6697
+ operations: ["disableOrganizationMember"],
6698
+ safety: "destructive",
6699
+ input: z.object({ member: memberField }),
6700
+ run: async ({ member }, run) => {
6701
+ const { organizationId, userId } = await chosenMember(run, member);
6702
+ return memberDisabled(run.api, organizationId, userId);
6703
+ }
6704
+ }),
6705
+ defineTool({
6706
+ name: "organization_member_enable",
6707
+ title: "Enable a member",
6708
+ description: "Let a disabled member sign in again. It takes an admin of the organization.",
6709
+ operations: ["enableOrganizationMember"],
6710
+ safety: "write",
6711
+ input: z.object({ member: memberField }),
6712
+ run: async ({ member }, run) => {
6713
+ const { organizationId, userId } = await chosenMember(run, member);
6714
+ return memberEnabled(run.api, organizationId, userId);
6715
+ }
6716
+ })
6717
+ ];
6718
+ //#endregion
5652
6719
  //#region src/commands/tools.ts
5653
6720
  const projectOption$1 = { project: { type: "string" } };
5654
6721
  const projectUsage$1 = [["--project <name>", "the project the tool belongs to, by slug, name or id"]];
@@ -5663,7 +6730,7 @@ const toolHints = {
5663
6730
  };
5664
6731
  const projectFrom$1 = async (run, given) => {
5665
6732
  if (given === void 0) throw new UsageError("This command needs a project.", "Pass --project <name>; kcloud projects list shows them.");
5666
- return resolveProjectId(run.api, given);
6733
+ return resolveProjectId(run, given);
5667
6734
  };
5668
6735
  const toolsOf = async (api, projectId) => dataOf("list the tools of the project", await api.GET("/projects/{id}/tools", { params: { path: { id: projectId } } }), toolHints).items;
5669
6736
  const resolveTool = async (api, projectId, reference) => {
@@ -5688,6 +6755,13 @@ const changedTool = async (api, projectId, tool, document) => {
5688
6755
  body: change
5689
6756
  }), toolHints);
5690
6757
  };
6758
+ const copiedTool = async (api, projectId, toolId, targetProjectId) => dataOf("copy the tool", await api.POST("/projects/{id}/tools/{toolId}/copy", {
6759
+ params: { path: {
6760
+ id: projectId,
6761
+ toolId
6762
+ } },
6763
+ body: { targetProjectId }
6764
+ }), toolHints);
5691
6765
  const createToolsCommand = (adaptersOf) => defineGroup({
5692
6766
  name: "tools",
5693
6767
  summary: "The HTTP APIs and MCP servers a project offers its agents.",
@@ -5833,6 +6907,31 @@ const createToolsCommand = (adaptersOf) => defineGroup({
5833
6907
  } } }), toolHints);
5834
6908
  return said(run, `Deleted the tool ${tool.name}.`);
5835
6909
  }
6910
+ }),
6911
+ defineCommand({
6912
+ name: "copy",
6913
+ summary: "Copy a tool to another project, without its secrets.",
6914
+ usage: "tools copy <tool> --project <name> --to <project>",
6915
+ optionsUsage: [...projectUsage$1, ["--to <project>", "the project that receives the copy, by slug, name or id"]],
6916
+ options: {
6917
+ ...projectOption$1,
6918
+ to: { type: "string" }
6919
+ },
6920
+ positionals: {
6921
+ min: 1,
6922
+ max: 1
6923
+ },
6924
+ run: async (invocation) => {
6925
+ const { values } = invocation;
6926
+ const target = values.to;
6927
+ if (target === void 0) throw new UsageError("tools copy needs --to <project>.", "kcloud projects list shows the projects.");
6928
+ const run = await runOf(adaptersOf, invocation);
6929
+ const projectId = await projectFrom$1(run, values.project);
6930
+ const tool = await resolveTool(run.api, projectId, invocation.positionals[0] ?? "");
6931
+ const targetProjectId = await resolveProjectId(run, target);
6932
+ const copied = await copiedTool(run.api, projectId, tool.id, targetProjectId);
6933
+ return printed(run, copied, (shown) => [`Copied ${tool.name} to ${target} as ${shown.id}.`, ...shown.complete ? [] : ["Its secrets stay behind. kcloud tools update --file sets them."]].join(" "));
6934
+ }
5836
6935
  })
5837
6936
  ]
5838
6937
  });
@@ -5906,6 +7005,23 @@ const projectToolTools = [
5906
7005
  });
5907
7006
  }
5908
7007
  }),
7008
+ defineTool({
7009
+ name: "tool_copy",
7010
+ title: "Copy a tool",
7011
+ description: "Copy a tool to another project of the organization. Its secrets stay behind, so a copy that needs them is incomplete until someone sets them.",
7012
+ operations: ["copyTool"],
7013
+ safety: "write",
7014
+ input: z.object({
7015
+ project: projectField,
7016
+ tool: toolField,
7017
+ to: projectField.describe("The project that receives the copy, by slug, name or id.")
7018
+ }),
7019
+ run: async ({ project, tool, to }, run) => {
7020
+ const projectId = await projectIdOf(run, project);
7021
+ const found = await resolveTool(run.api, projectId, tool);
7022
+ return copiedTool(run.api, projectId, found.id, await projectIdOf(run, to));
7023
+ }
7024
+ }),
5909
7025
  defineTool({
5910
7026
  name: "tool_delete",
5911
7027
  title: "Delete a tool",
@@ -5928,16 +7044,166 @@ const projectToolTools = [
5928
7044
  })
5929
7045
  ];
5930
7046
  //#endregion
5931
- //#region src/commands/projects.ts
5932
- const projectHint = { "project.not_found": "kcloud projects list shows the projects you are a member of." };
7047
+ //#region src/commands/projects/members.ts
7048
+ const projectMembersOf = async (api, projectId) => dataOf("list the members of the project", await api.GET("/projects/{id}/members", { params: { path: { id: projectId } } }), projectHint).items;
7049
+ const resolveProjectMember = async (api, projectId, reference) => {
7050
+ const found = (await projectMembersOf(api, projectId)).find((member) => member.userId === reference.toLowerCase() || sameEmail(member.email, reference));
7051
+ if (found === void 0) throw new CliError(`The project has no member ${reference}.`, "kcloud projects members list <project> shows them.");
7052
+ return found;
7053
+ };
7054
+ const projectUserIdOf = async (api, projectId, reference) => {
7055
+ if (isId(reference)) return reference.toLowerCase();
7056
+ const { organizationId } = await projectOf(api, projectId);
7057
+ return (await resolveOrganizationMember(api, organizationId, reference)).id;
7058
+ };
7059
+ const setProjectMember = async (api, projectId, userId, role) => {
7060
+ doneWith("set the member of the project", await api.PUT("/projects/{id}/members/{userId}", {
7061
+ params: { path: {
7062
+ id: projectId,
7063
+ userId
7064
+ } },
7065
+ body: { role }
7066
+ }), projectHint);
7067
+ };
7068
+ const removeProjectMember = async (api, projectId, userId) => {
7069
+ doneWith("remove the member of the project", await api.DELETE("/projects/{id}/members/{userId}", { params: { path: {
7070
+ id: projectId,
7071
+ userId
7072
+ } } }), projectHint);
7073
+ };
7074
+ const projectMembersCommand = (adaptersOf) => defineGroup({
7075
+ name: "members",
7076
+ summary: "The members of a project and their project role.",
7077
+ usage: "projects members <command>",
7078
+ subcommands: [
7079
+ defineCommand({
7080
+ name: "list",
7081
+ summary: "List the members of one project.",
7082
+ usage: "projects members list <project>",
7083
+ options: {},
7084
+ positionals: {
7085
+ min: 1,
7086
+ max: 1
7087
+ },
7088
+ run: async (invocation) => {
7089
+ const run = await runOf(adaptersOf, invocation);
7090
+ const id = await resolveProjectId(run, invocation.positionals[0] ?? "");
7091
+ const items = await projectMembersOf(run.api, id);
7092
+ return printed(run, items, (members) => members.length === 0 ? "The project has no member." : renderTable([
7093
+ { header: "EMAIL" },
7094
+ { header: "ROLE" },
7095
+ { header: "STATUS" },
7096
+ { header: "ADDED" },
7097
+ { header: "ID" }
7098
+ ], members.map((member) => [
7099
+ member.email,
7100
+ member.role,
7101
+ member.status,
7102
+ member.addedAt,
7103
+ member.userId
7104
+ ])));
7105
+ }
7106
+ }),
7107
+ defineCommand({
7108
+ name: "set",
7109
+ summary: "Add a member of the organization to a project, or change their role.",
7110
+ usage: "projects members set <project> <user> --role <role>",
7111
+ optionsUsage: [roleUsage, ["", "the user is named by e-mail or id, and belongs to the organization"]],
7112
+ options: roleOption,
7113
+ positionals: {
7114
+ min: 2,
7115
+ max: 2
7116
+ },
7117
+ run: async (invocation) => {
7118
+ const [project = "", user = ""] = invocation.positionals;
7119
+ const role = roleFrom(invocation.values.role, "projects members set");
7120
+ const run = await runOf(adaptersOf, invocation);
7121
+ const id = await resolveProjectId(run, project);
7122
+ const userId = await projectUserIdOf(run.api, id, user);
7123
+ await setProjectMember(run.api, id, userId, role);
7124
+ return said(run, `${user} is ${asRole(role)} of the project ${project} now.`);
7125
+ }
7126
+ }),
7127
+ defineCommand({
7128
+ name: "remove",
7129
+ summary: "Take a member off a project.",
7130
+ usage: "projects members remove <project> <user>",
7131
+ optionsUsage: [["", "the user is named by e-mail or id"]],
7132
+ options: {},
7133
+ positionals: {
7134
+ min: 2,
7135
+ max: 2
7136
+ },
7137
+ run: async (invocation) => {
7138
+ const [project = "", user = ""] = invocation.positionals;
7139
+ const run = await runOf(adaptersOf, invocation);
7140
+ const id = await resolveProjectId(run, project);
7141
+ const member = await resolveProjectMember(run.api, id, user);
7142
+ await confirmed({
7143
+ prompt: run.prompt,
7144
+ settings: run.settings,
7145
+ question: `Remove ${member.email} from the project ${project}?`
7146
+ });
7147
+ await removeProjectMember(run.api, id, member.userId);
7148
+ return said(run, `Removed ${member.email} from the project ${project}.`);
7149
+ }
7150
+ })
7151
+ ]
7152
+ });
7153
+ //#endregion
7154
+ //#region src/commands/projects/index.ts
7155
+ const fieldOptions = {
7156
+ slug: { type: "string" },
7157
+ description: { type: "string" },
7158
+ color: { type: "string" }
7159
+ };
7160
+ const fieldUsage = [
7161
+ ["--slug <slug>", "lowercase letters, digits and hyphens (default: made from the name)"],
7162
+ ["--description <text>", "what the project is for"],
7163
+ ["--color <#rrggbb>", `the color of the project in the panel (default: ${defaultProjectColor})`]
7164
+ ];
7165
+ const createdProject = async (scope, name, { slug, description, color }) => {
7166
+ const body = {
7167
+ name,
7168
+ ...slug === void 0 ? {} : { slug },
7169
+ description: description ?? "",
7170
+ color: color ?? "#7c86ff"
7171
+ };
7172
+ validated(createProjectBodySchema, body, "a project");
7173
+ const organization = await scope.organization();
7174
+ return {
7175
+ project: dataOf("create the project", await scope.api.POST("/organizations/{organizationId}/projects", {
7176
+ params: { path: { organizationId: organization.id } },
7177
+ body
7178
+ }), projectHint),
7179
+ organization
7180
+ };
7181
+ };
7182
+ const changedProject = async (api, id, change) => {
7183
+ const current = await projectOf(api, id);
7184
+ const body = {
7185
+ name: change.name ?? current.name,
7186
+ slug: change.slug ?? current.slug,
7187
+ description: change.description ?? current.description,
7188
+ color: change.color ?? current.color
7189
+ };
7190
+ validated(updateProjectBodySchema, body, "a change to a project");
7191
+ return dataOf("change the project", await api.PATCH("/projects/{id}", {
7192
+ params: { path: { id } },
7193
+ body
7194
+ }), projectHint);
7195
+ };
7196
+ const deleteProject = async (api, id) => {
7197
+ doneWith("delete the project", await api.DELETE("/projects/{id}", { params: { path: { id } } }), projectHint);
7198
+ };
5933
7199
  const createProjectsCommand = (adaptersOf) => defineGroup({
5934
7200
  name: "projects",
5935
- summary: "The projects you are a member of.",
7201
+ summary: "The projects of the organization you can see.",
5936
7202
  usage: "projects <command>",
5937
7203
  subcommands: [
5938
7204
  defineCommand({
5939
7205
  name: "list",
5940
- summary: "List the projects you can see.",
7206
+ summary: "List the projects of the organization you can see.",
5941
7207
  usage: "projects list",
5942
7208
  options: {},
5943
7209
  positionals: {
@@ -5946,8 +7212,9 @@ const createProjectsCommand = (adaptersOf) => defineGroup({
5946
7212
  },
5947
7213
  run: async (invocation) => {
5948
7214
  const run = await runOf(adaptersOf, invocation);
5949
- const { items } = dataOf("list the projects", await run.api.GET("/projects"));
5950
- return printed(run, items, (projects) => projects.length === 0 ? "You are a member of no project yet." : renderTable([
7215
+ const items = await projectsOf(run);
7216
+ const { slug } = await run.organization();
7217
+ return printed(run, items, (projects) => projects.length === 0 ? `You see no project in ${slug} yet.` : renderTable([
5951
7218
  { header: "SLUG" },
5952
7219
  { header: "NAME" },
5953
7220
  {
@@ -5976,12 +7243,13 @@ const createProjectsCommand = (adaptersOf) => defineGroup({
5976
7243
  },
5977
7244
  run: async (invocation) => {
5978
7245
  const run = await runOf(adaptersOf, invocation);
5979
- const id = await resolveProjectId(run.api, invocation.positionals[0] ?? "");
7246
+ const id = await resolveProjectId(run, invocation.positionals[0] ?? "");
5980
7247
  const project = dataOf("describe the project", await run.api.GET("/projects/{id}", { params: { path: { id } } }), projectHint);
5981
7248
  return printed(run, project, (shown) => renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
5982
7249
  ["name", shown.name],
5983
7250
  ["slug", shown.slug],
5984
7251
  ["description", shown.description === "" ? "-" : shown.description],
7252
+ ["color", shown.color],
5985
7253
  ["docker", shown.dockerAllowed ? "allowed" : "not allowed"],
5986
7254
  ["members", String(shown.memberCount)],
5987
7255
  ["created", shown.createdAt],
@@ -5990,9 +7258,52 @@ const createProjectsCommand = (adaptersOf) => defineGroup({
5990
7258
  }
5991
7259
  }),
5992
7260
  defineCommand({
5993
- name: "members",
5994
- summary: "List the members of one project.",
5995
- usage: "projects members <project>",
7261
+ name: "create",
7262
+ summary: "Create a project in the organization.",
7263
+ usage: "projects create <name> [--slug <slug>] [--description <text>] [--color <#rrggbb>]",
7264
+ optionsUsage: fieldUsage,
7265
+ options: fieldOptions,
7266
+ positionals: {
7267
+ min: 1,
7268
+ max: 1
7269
+ },
7270
+ run: async (invocation) => {
7271
+ const run = await runOf(adaptersOf, invocation);
7272
+ const { project, organization } = await createdProject(run, invocation.positionals[0] ?? "", invocation.values);
7273
+ return printed(run, project, (created) => `Created ${created.name} (${created.slug}) in ${organization.slug} as ${created.id}.`);
7274
+ }
7275
+ }),
7276
+ defineCommand({
7277
+ name: "update",
7278
+ summary: "Change the name, slug, description or color of a project.",
7279
+ usage: "projects update <project> [--name <name>] [--slug <slug>] [--description <text>] [--color <#rrggbb>]",
7280
+ optionsUsage: [["--name <name>", "the new name"], ...fieldUsage],
7281
+ options: {
7282
+ name: { type: "string" },
7283
+ ...fieldOptions
7284
+ },
7285
+ positionals: {
7286
+ min: 1,
7287
+ max: 1
7288
+ },
7289
+ run: async (invocation) => {
7290
+ const { values } = invocation;
7291
+ if ([
7292
+ values.name,
7293
+ values.slug,
7294
+ values.description,
7295
+ values.color
7296
+ ].every((value) => value === void 0)) throw new UsageError("projects update needs --name, --slug, --description or --color.");
7297
+ const run = await runOf(adaptersOf, invocation);
7298
+ const id = await resolveProjectId(run, invocation.positionals[0] ?? "");
7299
+ const project = await changedProject(run.api, id, values);
7300
+ return printed(run, project, (shown) => `Changed ${shown.name} (${shown.slug}).`);
7301
+ }
7302
+ }),
7303
+ defineCommand({
7304
+ name: "delete",
7305
+ summary: "Delete a project that holds no agent, credential, repository or tool.",
7306
+ usage: "projects delete <project>",
5996
7307
  options: {},
5997
7308
  positionals: {
5998
7309
  min: 1,
@@ -6000,50 +7311,136 @@ const createProjectsCommand = (adaptersOf) => defineGroup({
6000
7311
  },
6001
7312
  run: async (invocation) => {
6002
7313
  const run = await runOf(adaptersOf, invocation);
6003
- const id = await resolveProjectId(run.api, invocation.positionals[0] ?? "");
6004
- const { items } = dataOf("list the members of the project", await run.api.GET("/projects/{id}/members", { params: { path: { id } } }), projectHint);
6005
- return printed(run, items, (members) => members.length === 0 ? "The project has no member." : renderTable([
6006
- { header: "EMAIL" },
6007
- { header: "ROLE" },
6008
- { header: "STATUS" },
6009
- { header: "ADDED" }
6010
- ], members.map((member) => [
6011
- member.email,
6012
- member.role,
6013
- member.status,
6014
- member.addedAt
6015
- ])));
7314
+ const id = await resolveProjectId(run, invocation.positionals[0] ?? "");
7315
+ const project = await projectOf(run.api, id);
7316
+ await confirmed({
7317
+ prompt: run.prompt,
7318
+ settings: run.settings,
7319
+ question: `Delete the project ${project.name} (${project.slug})?`
7320
+ });
7321
+ await deleteProject(run.api, id);
7322
+ return said(run, `Deleted the project ${project.name}.`);
6016
7323
  }
6017
- })
7324
+ }),
7325
+ projectMembersCommand(adaptersOf)
6018
7326
  ]
6019
7327
  });
6020
7328
  //#endregion
6021
7329
  //#region src/mcp/tools/projects.ts
6022
- const projectTools = [defineTool({
6023
- name: "projects_list",
6024
- title: "List projects",
6025
- description: "List the projects you are a member of.",
6026
- operations: ["listProjects"],
6027
- safety: "read",
6028
- input: z.object({}),
6029
- run: async (_, run) => dataOf("list the projects", await run.api.GET("/projects")).items
6030
- }), defineTool({
6031
- name: "project_show",
6032
- title: "Show a project",
6033
- description: "Show one project together with its members.",
6034
- operations: ["getProject", "listProjectMembers"],
6035
- safety: "read",
6036
- input: z.object({ project: projectField }),
6037
- run: async ({ project }, run) => {
6038
- const id = await projectIdOf(run, project);
6039
- const shown = dataOf("describe the project", await run.api.GET("/projects/{id}", { params: { path: { id } } }), projectHint);
6040
- const { items } = dataOf("list the members of the project", await run.api.GET("/projects/{id}/members", { params: { path: { id } } }), projectHint);
6041
- return {
6042
- ...shown,
6043
- members: items
6044
- };
6045
- }
6046
- })];
7330
+ const userField = z.string().trim().min(1).describe("The user, by e-mail or id, a member of the organization of the project.");
7331
+ const fields = {
7332
+ slug: slugSchema.optional().describe("Lowercase letters, digits and hyphens."),
7333
+ description: z.string().max(500).optional(),
7334
+ color: colorSchema.optional().describe("The color in the panel, as #rrggbb.")
7335
+ };
7336
+ const projectTools = [
7337
+ defineTool({
7338
+ name: "projects_list",
7339
+ title: "List projects",
7340
+ description: "List the projects you can see in the organization this server acts in.",
7341
+ operations: ["listProjects"],
7342
+ safety: "read",
7343
+ input: z.object({}),
7344
+ run: async (_, run) => projectsOf(run)
7345
+ }),
7346
+ defineTool({
7347
+ name: "project_show",
7348
+ title: "Show a project",
7349
+ description: "Show one project together with its members.",
7350
+ operations: ["getProject", "listProjectMembers"],
7351
+ safety: "read",
7352
+ input: z.object({ project: projectField }),
7353
+ run: async ({ project }, run) => {
7354
+ const id = await projectIdOf(run, project);
7355
+ return {
7356
+ ...dataOf("describe the project", await run.api.GET("/projects/{id}", { params: { path: { id } } }), projectHint),
7357
+ members: await projectMembersOf(run.api, id)
7358
+ };
7359
+ }
7360
+ }),
7361
+ defineTool({
7362
+ name: "project_create",
7363
+ title: "Create a project",
7364
+ description: "Create a project in the organization this server acts in.",
7365
+ operations: ["createProject"],
7366
+ safety: "write",
7367
+ input: z.object({
7368
+ name: z.string().trim().min(2).max(80),
7369
+ ...fields
7370
+ }),
7371
+ run: async ({ name, ...rest }, run) => (await createdProject(run, name, rest)).project
7372
+ }),
7373
+ defineTool({
7374
+ name: "project_update",
7375
+ title: "Change a project",
7376
+ description: "Change the name, slug, description or color of a project. A field left out keeps its value.",
7377
+ operations: ["updateProject"],
7378
+ safety: "write",
7379
+ idempotent: true,
7380
+ input: z.object({
7381
+ project: projectField,
7382
+ name: z.string().trim().min(2).max(80).optional(),
7383
+ ...fields
7384
+ }),
7385
+ run: async ({ project, ...change }, run) => changedProject(run.api, await projectIdOf(run, project), change)
7386
+ }),
7387
+ defineTool({
7388
+ name: "project_delete",
7389
+ title: "Delete a project",
7390
+ description: "Delete a project. The instance refuses while it holds agents, credentials, repositories or tools.",
7391
+ operations: ["deleteProject"],
7392
+ safety: "destructive",
7393
+ input: z.object({ project: projectField }),
7394
+ run: async ({ project }, run) => {
7395
+ const id = await projectIdOf(run, project);
7396
+ await deleteProject(run.api, id);
7397
+ return { deleted: id };
7398
+ }
7399
+ }),
7400
+ defineTool({
7401
+ name: "project_member_set",
7402
+ title: "Set a member of a project",
7403
+ description: "Add a member of the organization to a project with a project role, or change the role of a member.",
7404
+ operations: ["setProjectMember"],
7405
+ safety: "write",
7406
+ idempotent: true,
7407
+ input: z.object({
7408
+ project: projectField,
7409
+ user: userField,
7410
+ role: projectRoleSchema
7411
+ }),
7412
+ run: async ({ project, user, role }, run) => {
7413
+ const id = await projectIdOf(run, project);
7414
+ const userId = await projectUserIdOf(run.api, id, user);
7415
+ await setProjectMember(run.api, id, userId, role);
7416
+ return {
7417
+ projectId: id,
7418
+ userId,
7419
+ role
7420
+ };
7421
+ }
7422
+ }),
7423
+ defineTool({
7424
+ name: "project_member_remove",
7425
+ title: "Remove a member of a project",
7426
+ description: "Take a member off a project. The account stays in the organization.",
7427
+ operations: ["removeProjectMember"],
7428
+ safety: "destructive",
7429
+ input: z.object({
7430
+ project: projectField,
7431
+ user: userField
7432
+ }),
7433
+ run: async ({ project, user }, run) => {
7434
+ const id = await projectIdOf(run, project);
7435
+ const member = await resolveProjectMember(run.api, id, user);
7436
+ await removeProjectMember(run.api, id, member.userId);
7437
+ return {
7438
+ projectId: id,
7439
+ removed: member.userId
7440
+ };
7441
+ }
7442
+ })
7443
+ ];
6047
7444
  //#endregion
6048
7445
  //#region src/mcp/tools/releases.ts
6049
7446
  const releaseTools = [defineTool({
@@ -6071,7 +7468,7 @@ const repositoryHints = {
6071
7468
  };
6072
7469
  const projectFrom = async (run, given) => {
6073
7470
  if (given === void 0) throw new UsageError("This command needs a project.", "Pass --project <name>; kcloud projects list shows them.");
6074
- return resolveProjectId(run.api, given);
7471
+ return resolveProjectId(run, given);
6075
7472
  };
6076
7473
  const repositoriesOf = async (api, projectId) => dataOf("list the repositories of the project", await api.GET("/projects/{id}/repositories", { params: { path: { id: projectId } } }), repositoryHints).items;
6077
7474
  const resolveRepositoryId = async (api, projectId, reference) => {
@@ -6447,23 +7844,28 @@ const taskTools = [
6447
7844
  //#region src/mcp/tools/index.ts
6448
7845
  const kcloudTools = [
6449
7846
  ...accountTools,
7847
+ ...organizationTools,
6450
7848
  ...projectTools,
6451
7849
  ...credentialTools,
6452
7850
  ...repositoryTools,
6453
7851
  ...projectToolTools,
7852
+ ...nodeTools,
6454
7853
  ...agentTools,
6455
7854
  ...taskTools,
6456
7855
  ...observabilityTools,
6457
7856
  ...notificationTools,
6458
7857
  ...releaseTools
6459
7858
  ];
7859
+ const passwordPrinted = (what) => `a secret never passes through the context of a model, and kcloud prints ${what}`;
7860
+ passwordPrinted("the one-time registration token of the node");
7861
+ passwordPrinted("the temporary password of the new member"), passwordPrinted("the new temporary password");
6460
7862
  //#endregion
6461
7863
  //#region src/mcp/server.ts
6462
7864
  const instructions = [
6463
- "These tools act on one Kikimora Cloud instance as the account kcloud is signed in with; whoami names both.",
7865
+ "These tools act on one Kikimora Cloud instance, as the account kcloud is signed in with, in one organization of it. whoami names all three.",
6464
7866
  "Name a project by its slug, its name or its id. Name an agent as <project>/<agent>, as its slug or name together with project, or by its id.",
6465
7867
  "A refusal quotes the error code of the instance. Its hint may name a kcloud command, such as kcloud agents list; the tool for the same resource and verb, agents_list, does that work here.",
6466
- "Secrets never pass through these tools. For them the person at the terminal runs kcloud credentials add, kcloud credentials rotate, kcloud tokens create, kcloud auth password, or sets the secrets of a tool in the panel."
7868
+ "Secrets never pass through these tools. For them the person at the terminal runs kcloud credentials add, kcloud credentials rotate, kcloud tokens create, kcloud auth password, kcloud orgs members add, kcloud orgs members reset-password, or sets the secrets of a tool in the panel."
6467
7869
  ].join("\n");
6468
7870
  const createKcloudServer = ({ version, open }) => {
6469
7871
  const server = new McpServer({
@@ -6484,8 +7886,8 @@ const createKcloudServer = ({ version, open }) => {
6484
7886
  const createMcpCommand = (adaptersOf, cliVersion) => defineCommand({
6485
7887
  name: "mcp",
6486
7888
  summary: "Serve the work of kcloud to an MCP client over stdio.",
6487
- usage: "mcp [--profile <name>] [--url <origin>]",
6488
- optionsUsage: [["", "an MCP client starts it, as in: claude mcp add kcloud -- kcloud mcp"], ["", "each tool call uses the profile or KIKIMORA_CLOUD_TOKEN of that moment"]],
7889
+ usage: "mcp [--url <origin>] [--org <organization>]",
7890
+ optionsUsage: [["", "an MCP client starts it, as in: claude mcp add kcloud -- kcloud mcp"], ["", "each tool call reads the token and the organization of that moment"]],
6489
7891
  options: {},
6490
7892
  positionals: {
6491
7893
  min: 0,
@@ -6504,13 +7906,12 @@ const createMcpCommand = (adaptersOf, cliVersion) => defineCommand({
6504
7906
  });
6505
7907
  return {
6506
7908
  api: session.api,
7909
+ me: session.me,
7910
+ organizations: session.organizations,
7911
+ organization: session.organization,
6507
7912
  origin: session.origin,
6508
- profileName: session.profileName,
6509
7913
  kcloudVersion: cliVersion,
6510
- token: () => tokenOf({
6511
- settings,
6512
- context
6513
- }),
7914
+ token: () => Promise.resolve(session.token),
6514
7915
  wait: adapters.wait,
6515
7916
  now: adapters.now
6516
7917
  };
@@ -6527,96 +7928,6 @@ const createMcpCommand = (adaptersOf, cliVersion) => defineCommand({
6527
7928
  }
6528
7929
  });
6529
7930
  //#endregion
6530
- //#region src/commands/profiles.ts
6531
- const rowsOf = (store) => Object.entries(store.profiles).map(([name, profile]) => [
6532
- name === store.current ? `${name} *` : name,
6533
- profile.origin,
6534
- profile.email ?? "unknown"
6535
- ]);
6536
- const namedProfile = (store, name) => {
6537
- if (store.profiles[name] === void 0) {
6538
- const known = Object.keys(store.profiles);
6539
- throw new CliError(`There is no profile ${name} here.`, known.length === 0 ? "Run kcloud login --url <origin> to make one." : `kcloud profiles list shows ${known.join(", ")}.`);
6540
- }
6541
- };
6542
- const createProfilesCommand = () => defineGroup({
6543
- name: "profiles",
6544
- summary: "The instances this machine is signed in to.",
6545
- usage: "profiles <command>",
6546
- subcommands: [
6547
- defineCommand({
6548
- name: "list",
6549
- summary: "List the saved profiles, marking the current one.",
6550
- usage: "profiles list",
6551
- options: {},
6552
- positionals: {
6553
- min: 0,
6554
- max: 0
6555
- },
6556
- run: async ({ settings, context }) => {
6557
- const store = await readProfileStore(context.homeDirectory);
6558
- if (settings.json) {
6559
- context.terminal.json({
6560
- current: store.current,
6561
- profiles: Object.entries(store.profiles).map(([name, profile]) => ({
6562
- name,
6563
- origin: profile.origin,
6564
- email: profile.email
6565
- }))
6566
- });
6567
- return exitCodes.ok;
6568
- }
6569
- const rows = rowsOf(store);
6570
- context.terminal.line(rows.length === 0 ? "No instance is signed in here; kcloud login --url <origin> signs in." : renderTable([
6571
- { header: "PROFILE" },
6572
- { header: "INSTANCE" },
6573
- { header: "ACCOUNT" }
6574
- ], rows));
6575
- return exitCodes.ok;
6576
- }
6577
- }),
6578
- defineCommand({
6579
- name: "use",
6580
- summary: "Make one of the saved profiles the current one.",
6581
- usage: "profiles use <name>",
6582
- options: {},
6583
- positionals: {
6584
- min: 1,
6585
- max: 1
6586
- },
6587
- run: async ({ positionals, context }) => {
6588
- const [name = ""] = positionals;
6589
- const store = await readProfileStore(context.homeDirectory);
6590
- namedProfile(store, name);
6591
- await writeProfileStore(context.homeDirectory, {
6592
- ...store,
6593
- current: name
6594
- });
6595
- context.terminal.line(`${name} is the current profile.`);
6596
- return exitCodes.ok;
6597
- }
6598
- }),
6599
- defineCommand({
6600
- name: "remove",
6601
- summary: "Forget a saved profile without revoking its token.",
6602
- usage: "profiles remove <name>",
6603
- options: {},
6604
- positionals: {
6605
- min: 1,
6606
- max: 1
6607
- },
6608
- run: async ({ positionals, context }) => {
6609
- const [name = ""] = positionals;
6610
- const store = await readProfileStore(context.homeDirectory);
6611
- namedProfile(store, name);
6612
- await writeProfileStore(context.homeDirectory, withoutProfile(store, name));
6613
- context.terminal.line(`Forgot ${name}; its token stays usable until it is revoked.`);
6614
- return exitCodes.ok;
6615
- }
6616
- })
6617
- ]
6618
- });
6619
- //#endregion
6620
7931
  //#region src/commands/releases.ts
6621
7932
  const createReleasesCommand = (adaptersOf) => defineGroup({
6622
7933
  name: "releases",
@@ -6656,13 +7967,14 @@ const kcloudCommands = () => [
6656
7967
  createLogoutCommand(createAdapters),
6657
7968
  createWhoamiCommand(createAdapters),
6658
7969
  createVersionCommand(createAdapters, cliVersion),
6659
- createProfilesCommand(),
7970
+ createOrgsCommand(createAdapters),
6660
7971
  createTokensCommand(createAdapters),
6661
7972
  createAuthCommand(createAdapters),
6662
7973
  createProjectsCommand(createAdapters),
6663
7974
  createCredentialsCommand(createAdapters),
6664
7975
  createRepositoriesCommand(createAdapters),
6665
7976
  createToolsCommand(createAdapters),
7977
+ createNodesCommand(createAdapters),
6666
7978
  createAgentsCommand(createAdapters),
6667
7979
  createNotificationsCommand(createAdapters),
6668
7980
  createReleasesCommand(createAdapters),