@kikimoradev/kcloud 0.12.1 → 0.14.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 +1323 -463
  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",
@@ -1354,7 +1364,7 @@ const costPointSchema = z.object({
1354
1364
  costUsd: z.number().min(0),
1355
1365
  runs: z.int().nonnegative()
1356
1366
  }).meta({ id: "CostPoint" });
1357
- z.object({
1367
+ const costSeriesSchema = z.object({
1358
1368
  items: z.array(costPointSchema),
1359
1369
  totalCostUsd: z.number().nonnegative(),
1360
1370
  totalRuns: z.int().nonnegative(),
@@ -1364,6 +1374,12 @@ const costQuerySchema = z.object({
1364
1374
  from: z.iso.date(),
1365
1375
  to: z.iso.date()
1366
1376
  });
1377
+ const projectCostSchema = z.object({
1378
+ projectId: idSchema,
1379
+ costUsd: z.number().min(0),
1380
+ runs: z.int().nonnegative()
1381
+ }).meta({ id: "ProjectCost" });
1382
+ costSeriesSchema.extend({ projects: z.array(projectCostSchema) }).meta({ id: "OrganizationCosts" });
1367
1383
  pageSchema(z.object({
1368
1384
  taskId: z.string(),
1369
1385
  title: z.string(),
@@ -1381,18 +1397,34 @@ costQuerySchema.extend({ cursor: cursorSchema.optional() });
1381
1397
  const emailSchema = z.email().max(254);
1382
1398
  const passwordSchema = z.string().min(12).max(128);
1383
1399
  const userStatusSchema = z.enum(["active", "disabled"]);
1384
- const meSchema = z.object({
1400
+ const organizationNameSchema = z.string().trim().min(2).max(80);
1401
+ const organizationSlugSchema = z.string().regex(slugPattern);
1402
+ const organizationSchema = z.object({
1385
1403
  id: idSchema,
1404
+ name: z.string(),
1405
+ slug: organizationSlugSchema,
1406
+ createdAt: z.iso.datetime()
1407
+ }).meta({ id: "Organization" });
1408
+ z.object({ items: z.array(organizationSchema) }).meta({ id: "OrganizationList" });
1409
+ z.object({ organizationId: idSchema });
1410
+ z.object({
1386
1411
  organizationId: idSchema,
1412
+ userId: idSchema
1413
+ });
1414
+ const meSchema = z.object({
1415
+ id: idSchema,
1387
1416
  email: emailSchema,
1388
- role: roleSchema,
1417
+ organization: organizationSchema,
1418
+ organizationRole: organizationRoleSchema,
1419
+ platformAdmin: z.boolean(),
1389
1420
  locale: localeSchema,
1390
1421
  mustChangePassword: z.boolean()
1391
1422
  }).meta({ id: "Me" });
1392
1423
  const userSchema = z.object({
1393
1424
  id: idSchema,
1394
1425
  email: emailSchema,
1395
- role: roleSchema,
1426
+ organizationRole: organizationRoleSchema,
1427
+ platformAdmin: z.boolean(),
1396
1428
  status: userStatusSchema,
1397
1429
  locale: localeSchema,
1398
1430
  mustChangePassword: z.boolean(),
@@ -1408,20 +1440,33 @@ z.object({
1408
1440
  newPassword: passwordSchema
1409
1441
  }).meta({ id: "ChangePasswordBody" });
1410
1442
  z.object({ locale: localeSchema }).meta({ id: "ChangeLocaleBody" });
1411
- z.object({
1443
+ const createUserBodySchema = z.object({
1412
1444
  email: emailSchema,
1413
- role: roleSchema,
1445
+ organizationRole: organizationRoleSchema,
1414
1446
  locale: localeSchema,
1415
1447
  initialPassword: passwordSchema.optional()
1416
1448
  }).meta({ id: "CreateUserBody" });
1417
- z.object({
1449
+ const createdUserSchema = z.object({
1418
1450
  user: userSchema,
1419
1451
  temporaryPassword: z.string().nullable()
1420
1452
  }).meta({ id: "CreatedUser" });
1421
- z.object({ role: roleSchema }).meta({ id: "UpdateUserBody" });
1453
+ z.object({ organizationRole: organizationRoleSchema }).meta({ id: "UpdateUserBody" });
1454
+ z.object({
1455
+ name: organizationNameSchema,
1456
+ slug: organizationSlugSchema.optional(),
1457
+ admin: z.object({
1458
+ email: emailSchema,
1459
+ locale: localeSchema,
1460
+ initialPassword: passwordSchema.optional()
1461
+ })
1462
+ }).meta({ id: "CreateOrganizationBody" });
1422
1463
  z.object({ temporaryPassword: z.string() }).meta({ id: "TemporaryPassword" });
1423
1464
  z.object({ items: z.array(userSchema) }).meta({ id: "UserList" });
1424
1465
  z.object({ id: idSchema });
1466
+ z.object({
1467
+ organization: organizationSchema,
1468
+ admin: createdUserSchema
1469
+ }).meta({ id: "CreatedOrganization" });
1425
1470
  const accessTokenNameSchema = z.string().trim().min(1).max(80);
1426
1471
  const accessTokenSchema = z.object({
1427
1472
  id: idSchema,
@@ -1481,6 +1526,8 @@ z.discriminatedUnion("status", [cliTokenWaitingSchema, cliTokenReadySchema]).met
1481
1526
  z.object({ version: z.string() }).meta({ id: "PlatformVersion" });
1482
1527
  const auditActionSchema = z.enum([
1483
1528
  "platform.bootstrapped",
1529
+ "organization.created",
1530
+ "organization.renamed",
1484
1531
  "user.created",
1485
1532
  "user.role_changed",
1486
1533
  "user.disabled",
@@ -1495,7 +1542,9 @@ const auditActionSchema = z.enum([
1495
1542
  "project.created",
1496
1543
  "project.updated",
1497
1544
  "project.member_added",
1545
+ "project.member_role_changed",
1498
1546
  "project.member_removed",
1547
+ "project.deleted",
1499
1548
  "project.docker_changed",
1500
1549
  "credential.created",
1501
1550
  "credential.rotated",
@@ -1549,6 +1598,7 @@ const auditTargetTypeSchema = z.enum([
1549
1598
  ]);
1550
1599
  const auditEntrySchema = z.object({
1551
1600
  id: idSchema,
1601
+ organizationId: idSchema.nullable(),
1552
1602
  occurredAt: z.iso.datetime(),
1553
1603
  actor: z.object({
1554
1604
  kind: z.enum(["user", "system"]),
@@ -1565,6 +1615,7 @@ const auditEntrySchema = z.object({
1565
1615
  ip: z.string().nullable()
1566
1616
  }).meta({ id: "AuditEntry" });
1567
1617
  cursorQuerySchema.extend({
1618
+ organizationId: idSchema.optional(),
1568
1619
  action: auditActionSchema.optional(),
1569
1620
  actorUserId: idSchema.optional(),
1570
1621
  from: z.iso.datetime().optional(),
@@ -1577,6 +1628,7 @@ const slugSchema = z.string().regex(slugPattern);
1577
1628
  const colorSchema = z.string().regex(hexColorPattern);
1578
1629
  const projectSchema = z.object({
1579
1630
  id: idSchema,
1631
+ organizationId: idSchema,
1580
1632
  name: z.string(),
1581
1633
  slug: slugSchema,
1582
1634
  description: z.string(),
@@ -1589,26 +1641,27 @@ const projectSchema = z.object({
1589
1641
  const memberSchema = z.object({
1590
1642
  userId: idSchema,
1591
1643
  email: z.email(),
1592
- role: roleSchema,
1644
+ role: projectRoleSchema,
1593
1645
  status: userStatusSchema,
1594
1646
  addedAt: z.iso.datetime()
1595
1647
  }).meta({ id: "Member" });
1596
1648
  projectSchema.extend({ members: z.array(memberSchema) }).meta({ id: "ProjectDetail" });
1597
1649
  z.object({ items: z.array(projectSchema) }).meta({ id: "ProjectList" });
1598
1650
  z.object({ items: z.array(memberSchema) }).meta({ id: "MemberList" });
1599
- z.object({
1651
+ const createProjectBodySchema = z.object({
1600
1652
  name: z.string().trim().min(2).max(80),
1601
1653
  slug: slugSchema.optional(),
1602
1654
  description: z.string().max(500),
1603
1655
  color: colorSchema
1604
1656
  }).meta({ id: "CreateProjectBody" });
1605
- z.object({
1657
+ const updateProjectBodySchema = z.object({
1606
1658
  name: z.string().trim().min(2).max(80),
1607
1659
  slug: slugSchema,
1608
1660
  description: z.string().max(500),
1609
1661
  color: colorSchema
1610
1662
  }).meta({ id: "UpdateProjectBody" });
1611
1663
  z.object({ allowed: z.boolean() }).meta({ id: "SetProjectDockerBody" });
1664
+ z.object({ role: projectRoleSchema }).meta({ id: "SetProjectMemberBody" });
1612
1665
  z.object({ id: idSchema });
1613
1666
  z.object({
1614
1667
  id: idSchema,
@@ -2842,10 +2895,90 @@ const createAdapters = (context) => ({
2842
2895
  clientName: () => `kcloud on ${hostname()}`
2843
2896
  });
2844
2897
  //#endregion
2845
- //#region src/profile/config.ts
2898
+ //#region src/resolve.ts
2899
+ const organizationHints = { "organization.not_found": "kcloud orgs list shows the organizations you can see." };
2900
+ const isId = (reference) => idSchema.safeParse(reference).success;
2901
+ const once = (load) => {
2902
+ let loaded = null;
2903
+ return () => loaded ??= load();
2904
+ };
2905
+ const slugsOf = (items) => items.map(({ slug }) => slug).join(", ");
2906
+ const organizationIn = (organizations, reference) => {
2907
+ const found = organizations.filter((organization) => organization.id === reference.toLowerCase() || organization.slug === reference || organization.name === reference);
2908
+ const [first] = found;
2909
+ 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)}.`);
2910
+ if (found.length > 1) throw new CliError(`${reference} names ${found.length} organizations.`, "Name it by its slug or by its id instead.");
2911
+ return first;
2912
+ };
2913
+ const scopeOf = (api, reference) => {
2914
+ const me = once(async () => dataOf("say who the token belongs to", await api.GET("/auth/me")));
2915
+ const organizations = once(async () => dataOf("list the organizations", await api.GET("/organizations")).items);
2916
+ return {
2917
+ api,
2918
+ me,
2919
+ organizations,
2920
+ organization: once(async () => reference === null ? (await me()).organization : organizationIn(await organizations(), reference))
2921
+ };
2922
+ };
2923
+ const projectsOf = async (scope) => {
2924
+ const { id } = await scope.organization();
2925
+ return dataOf("list the projects", await scope.api.GET("/organizations/{organizationId}/projects", { params: { path: { organizationId: id } } }), organizationHints).items;
2926
+ };
2927
+ const matches = (reference, project) => project.slug === reference || project.name === reference;
2928
+ const resolveProjectId = async (scope, reference) => {
2929
+ if (isId(reference)) return reference;
2930
+ const projects = await projectsOf(scope);
2931
+ const found = projects.filter((project) => matches(reference, project));
2932
+ const [first] = found;
2933
+ if (first === void 0) {
2934
+ const { slug } = await scope.organization();
2935
+ 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)}.`);
2936
+ }
2937
+ if (found.length > 1) throw new CliError(`${reference} names ${found.length} projects.`, "Name it by its slug or by its id instead.");
2938
+ return first.id;
2939
+ };
2940
+ const agentsOfProject = async (api, projectId) => dataOf("list the agents of the project", await api.GET("/projects/{id}/agents", { params: { path: { id: projectId } } })).items;
2941
+ const resolveAgentId = async (scope, reference, project) => {
2942
+ if (isId(reference)) return reference;
2943
+ const [left, right] = reference.split("/");
2944
+ const projectReference = right === void 0 ? project : left;
2945
+ const agentReference = right ?? left ?? "";
2946
+ const searched = projectReference === void 0 ? await projectsOf(scope) : [{
2947
+ id: await resolveProjectId(scope, projectReference),
2948
+ slug: projectReference
2949
+ }];
2950
+ const found = [];
2951
+ for (const candidate of searched) for (const agent of await agentsOfProject(scope.api, candidate.id)) if (agent.slug === agentReference || agent.name === agentReference) found.push({
2952
+ agent,
2953
+ project: candidate
2954
+ });
2955
+ const [first] = found;
2956
+ 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.");
2957
+ if (found.length > 1) throw new CliError(`${reference} names an agent in ${found.map(({ project }) => project.slug).join(", ")}.`, "Name it as <project>/<agent> instead.");
2958
+ return first.agent.id;
2959
+ };
2960
+ const sameEmail = (left, right) => left.toLowerCase() === right.toLowerCase();
2961
+ const organizationMembersOf = async (api, organizationId) => dataOf("list the members of the organization", await api.GET("/organizations/{organizationId}/members", { params: { path: { organizationId } } }), organizationHints).items;
2962
+ const resolveOrganizationMember = async (api, organizationId, reference) => {
2963
+ const found = (await organizationMembersOf(api, organizationId)).find((member) => member.id === reference.toLowerCase() || sameEmail(member.email, reference));
2964
+ if (found === void 0) throw new CliError(`The organization has no member ${reference}.`, "kcloud orgs members list shows them.");
2965
+ return found;
2966
+ };
2967
+ //#endregion
2968
+ //#region src/instance/config.ts
2846
2969
  const configFileName = "cli.json";
2847
2970
  const stateDirectoryName = ".kikimora-cloud";
2848
- const profileFileOf = (homeDirectory) => join(homeDirectory, stateDirectoryName, configFileName);
2971
+ const configFileOf = (homeDirectory) => join(homeDirectory, stateDirectoryName, configFileName);
2972
+ const instanceSchema = z.strictObject({
2973
+ token: z.string().min(1),
2974
+ tokenId: z.string().min(1).nullable(),
2975
+ email: z.string().min(1).nullable(),
2976
+ organization: z.string().min(1).nullable()
2977
+ });
2978
+ const instanceStoreSchema = z.strictObject({
2979
+ version: z.literal(2),
2980
+ instances: z.record(z.url(), instanceSchema)
2981
+ });
2849
2982
  const profileSchema = z.strictObject({
2850
2983
  origin: z.url(),
2851
2984
  token: z.string().min(1),
@@ -2857,97 +2990,88 @@ const profileStoreSchema = z.strictObject({
2857
2990
  current: z.string().min(1).nullable(),
2858
2991
  profiles: z.record(z.string().min(1), profileSchema)
2859
2992
  });
2860
- const emptyProfileStore = {
2861
- version: 1,
2862
- current: null,
2863
- profiles: {}
2993
+ const storedSchema = z.discriminatedUnion("version", [instanceStoreSchema, profileStoreSchema]);
2994
+ const emptyInstanceStore = {
2995
+ version: 2,
2996
+ instances: {}
2997
+ };
2998
+ const instancesOfProfiles = ({ current, profiles }) => {
2999
+ const currentLast = Object.entries(profiles).toSorted(([left], [right]) => Number(left === current) - Number(right === current));
3000
+ return {
3001
+ version: 2,
3002
+ instances: Object.fromEntries(currentLast.map(([, { origin, token, tokenId, email }]) => [new URL(origin).origin, {
3003
+ token,
3004
+ tokenId,
3005
+ email,
3006
+ organization: null
3007
+ }]))
3008
+ };
2864
3009
  };
2865
- const readProfileStore = async (homeDirectory) => {
2866
- const path = profileFileOf(homeDirectory);
3010
+ const readInstanceStore = async (homeDirectory) => {
3011
+ const path = configFileOf(homeDirectory);
2867
3012
  const content = await readPrivateFile(path);
2868
- if (content === null) return emptyProfileStore;
3013
+ if (content === null) return emptyInstanceStore;
2869
3014
  let document;
2870
3015
  try {
2871
3016
  document = JSON.parse(content);
2872
3017
  } catch {
2873
3018
  throw new CliError(`${path} is not JSON.`, `Fix it or delete it, then log in again.`);
2874
3019
  }
2875
- const parsed = profileStoreSchema.safeParse(document);
3020
+ const parsed = storedSchema.safeParse(document);
2876
3021
  if (!parsed.success) throw new CliError(`${path} is not what ${configFileName} should hold.`, z.prettifyError(parsed.error));
2877
- return parsed.data;
3022
+ return parsed.data.version === 2 ? parsed.data : instancesOfProfiles(parsed.data);
2878
3023
  };
2879
- const writeProfileStore = async (homeDirectory, store) => {
2880
- await writePrivateFile(profileFileOf(homeDirectory), `${JSON.stringify(store, null, 2)}\n`);
3024
+ const writeInstanceStore = async (homeDirectory, store) => {
3025
+ await writePrivateFile(configFileOf(homeDirectory), `${JSON.stringify(store, null, 2)}\n`);
2881
3026
  };
2882
- const withProfile = (store, name, profile) => ({
2883
- version: 1,
2884
- current: name,
2885
- profiles: {
2886
- ...store.profiles,
2887
- [name]: profile
3027
+ const withInstance = (store, origin, instance) => ({
3028
+ version: 2,
3029
+ instances: {
3030
+ ...store.instances,
3031
+ [origin]: instance
2888
3032
  }
2889
3033
  });
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
- };
3034
+ const withoutInstance = (store, origin) => ({
3035
+ version: 2,
3036
+ instances: Object.fromEntries(Object.entries(store.instances).filter(([candidate]) => candidate !== origin))
3037
+ });
2903
3038
  //#endregion
2904
- //#region src/profile/session.ts
3039
+ //#region src/instance/session.ts
2905
3040
  const urlVariable = "KIKIMORA_CLOUD_URL";
2906
3041
  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];
3042
+ const organizationVariable = "KIKIMORA_CLOUD_ORG";
3043
+ const givenIn = (environment, name) => {
3044
+ const value = environment[name]?.trim();
3045
+ return value === void 0 || value === "" ? null : value;
2915
3046
  };
2916
- const currentProfileOf = (store) => {
2917
- if (store.current === null) throw notSignedIn();
2918
- return namedProfileOf(store, store.current);
3047
+ const originFor = (settings, context) => originOf(settings.origin ?? givenIn(context.environment, "KIKIMORA_CLOUD_URL") ?? "https://cloud.kikimora.dev");
3048
+ const notSignedIn = (origin, store) => {
3049
+ const known = Object.keys(store.instances);
3050
+ 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
3051
  };
2920
3052
  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);
3053
+ const origin = originFor(settings, context);
3054
+ const store = await readInstanceStore(context.homeDirectory);
3055
+ const stored = store.instances[origin] ?? null;
3056
+ const fromEnvironment = givenIn(context.environment, tokenVariable);
3057
+ const token = fromEnvironment ?? stored?.token ?? null;
3058
+ if (token === null) throw notSignedIn(origin, store);
3059
+ const organization = settings.organization ?? givenIn(context.environment, "KIKIMORA_CLOUD_ORG") ?? stored?.organization ?? null;
2938
3060
  return {
2939
- api: api({
3061
+ ...scopeOf(api({
2940
3062
  origin,
2941
- token: profile.token
2942
- }),
3063
+ token
3064
+ }), organization),
2943
3065
  origin,
2944
- profileName: name,
2945
- store
3066
+ token,
3067
+ tokenFromEnvironment: fromEnvironment !== null,
3068
+ store,
3069
+ stored
2946
3070
  };
2947
3071
  };
2948
3072
  //#endregion
2949
3073
  //#region src/commands/auth/account.ts
2950
- const passwordLength = `between ${passwordSchema.minLength ?? 0} and ${passwordSchema.maxLength ?? 0} characters`;
3074
+ const passwordLength$1 = `between ${passwordSchema.minLength ?? 0} and ${passwordSchema.maxLength ?? 0} characters`;
2951
3075
  const createAuthCommand = (adaptersOf) => defineGroup({
2952
3076
  name: "auth",
2953
3077
  summary: "Your own account on this instance.",
@@ -2975,8 +3099,8 @@ const createAuthCommand = (adaptersOf) => defineGroup({
2975
3099
  validate: (value) => value === "" ? "cannot be empty" : void 0
2976
3100
  });
2977
3101
  const newPassword = await prompt.password({
2978
- message: `Your new password, ${passwordLength}`,
2979
- validate: (value) => passwordSchema.safeParse(value).success ? void 0 : passwordLength
3102
+ message: `Your new password, ${passwordLength$1}`,
3103
+ validate: (value) => passwordSchema.safeParse(value).success ? void 0 : passwordLength$1
2980
3104
  });
2981
3105
  doneWith("change the password", await session.api.PATCH("/auth/me/password", { body: {
2982
3106
  currentPassword,
@@ -3019,16 +3143,7 @@ const createAuthCommand = (adaptersOf) => defineGroup({
3019
3143
  });
3020
3144
  //#endregion
3021
3145
  //#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
- };
3146
+ const usage = "login [--url <origin>] [--with-token]";
3032
3147
  const isTerminal$2 = (stream) => stream.isTTY === true;
3033
3148
  const pipedText$2 = async (context) => {
3034
3149
  if (isTerminal$2(context.io.stdin)) return "";
@@ -3037,8 +3152,8 @@ const pipedText$2 = async (context) => {
3037
3152
  return Buffer.concat(chunks).toString("utf8").trim();
3038
3153
  };
3039
3154
  const readToken = async (context, settings) => {
3040
- const fromEnvironment = context.environment[tokenVariable];
3041
- if (fromEnvironment !== void 0 && fromEnvironment !== "") return fromEnvironment;
3155
+ const fromEnvironment = givenIn(context.environment, tokenVariable);
3156
+ if (fromEnvironment !== null) return fromEnvironment;
3042
3157
  const piped = await pipedText$2(context);
3043
3158
  if (piped !== "") return piped;
3044
3159
  if (settings.yes) throw new UsageError("login --with-token found no token to use.", `Pipe it on stdin or set ${tokenVariable}.`);
@@ -3046,9 +3161,13 @@ const readToken = async (context, settings) => {
3046
3161
  };
3047
3162
  const createLoginCommand = (adaptersOf) => defineCommand({
3048
3163
  name: "login",
3049
- summary: "Sign in to an instance and save its token in a profile.",
3164
+ summary: "Sign in to an instance and save its token for its address.",
3050
3165
  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}`]],
3166
+ optionsUsage: [
3167
+ ["--with-token", "take a token from stdin or the environment instead of a browser"],
3168
+ ["", `the address comes from --url or ${urlVariable}, or is ${defaultOrigin}`],
3169
+ ["", `the token of --with-token comes from stdin or ${tokenVariable}`]
3170
+ ],
3052
3171
  options: { "with-token": {
3053
3172
  type: "boolean",
3054
3173
  default: false
@@ -3059,7 +3178,7 @@ const createLoginCommand = (adaptersOf) => defineCommand({
3059
3178
  },
3060
3179
  run: async ({ values, settings, context }) => {
3061
3180
  const adapters = adaptersOf(context);
3062
- const origin = await originFor(settings, context);
3181
+ const origin = originFor(settings, context);
3063
3182
  const prompt = adapters.prompter(settings);
3064
3183
  const anonymous = adapters.api({
3065
3184
  origin,
@@ -3079,16 +3198,16 @@ const createLoginCommand = (adaptersOf) => defineCommand({
3079
3198
  const me = dataOf("say who the token belongs to", await identified.GET("/auth/me"));
3080
3199
  await save({
3081
3200
  context,
3082
- settings,
3083
3201
  origin,
3084
3202
  secret,
3085
3203
  me
3086
3204
  });
3087
3205
  const version = await instanceVersion(identified);
3088
- prompt.outro(`Signed in to ${origin} as ${me.email}${version === null ? "" : ` on ${version}`}.`);
3206
+ prompt.outro(`Signed in to ${origin} as ${me.email} of ${me.organization.name}${version === null ? "" : ` on ${version}`}.`);
3089
3207
  if (settings.json) context.terminal.json({
3090
3208
  origin,
3091
3209
  email: me.email,
3210
+ organization: me.organization.slug,
3092
3211
  version
3093
3212
  });
3094
3213
  return exitCodes.ok;
@@ -3124,14 +3243,14 @@ const authorizeInBrowser = async ({ adapters, api, origin, prompt }) => {
3124
3243
  }
3125
3244
  }
3126
3245
  };
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,
3246
+ const save = async ({ context, origin, secret, me }) => {
3247
+ const store = await readInstanceStore(context.homeDirectory);
3248
+ const previous = store.instances[origin];
3249
+ await writeInstanceStore(context.homeDirectory, withInstance(store, origin, {
3132
3250
  token: secret,
3133
3251
  tokenId: null,
3134
- email: me.email
3252
+ email: me.email,
3253
+ organization: previous?.email === me.email ? previous.organization : null
3135
3254
  }));
3136
3255
  };
3137
3256
  const instanceVersion = async (api) => {
@@ -3140,11 +3259,15 @@ const instanceVersion = async (api) => {
3140
3259
  };
3141
3260
  //#endregion
3142
3261
  //#region src/commands/auth/logout.ts
3262
+ const outcomeOf = (session, kept) => {
3263
+ 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}.`;
3264
+ return kept ? `Forgot ${session.origin} here. Its token is still usable.` : `Revoked the token of ${session.origin} and forgot it here.`;
3265
+ };
3143
3266
  const createLogoutCommand = (adaptersOf) => defineCommand({
3144
3267
  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"]],
3268
+ summary: "Revoke the token of an instance and forget it here.",
3269
+ usage: "logout [--url <origin>] [--keep-token]",
3270
+ optionsUsage: [["--keep-token", "forget the instance here and leave its token usable elsewhere"]],
3148
3271
  options: { "keep-token": {
3149
3272
  type: "boolean",
3150
3273
  default: false
@@ -3160,12 +3283,10 @@ const createLogoutCommand = (adaptersOf) => defineCommand({
3160
3283
  context,
3161
3284
  api: adapters.api
3162
3285
  });
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.`);
3286
+ const kept = values["keep-token"];
3287
+ if (!kept) doneWith("revoke the token", await session.api.POST("/auth/logout"));
3288
+ if (!session.tokenFromEnvironment) await writeInstanceStore(context.homeDirectory, withoutInstance(session.store, session.origin));
3289
+ context.terminal.line(outcomeOf(session, kept));
3169
3290
  return exitCodes.ok;
3170
3291
  }
3171
3292
  });
@@ -3228,9 +3349,10 @@ const createVersionCommand = (adaptersOf, cliVersion) => defineCommand({
3228
3349
  });
3229
3350
  //#endregion
3230
3351
  //#region src/commands/auth/whoami.ts
3352
+ const organizationNameOf = ({ name, slug }) => `${name} (${slug})`;
3231
3353
  const createWhoamiCommand = (adaptersOf) => defineCommand({
3232
3354
  name: "whoami",
3233
- summary: "Say which instance and which account the token belongs to.",
3355
+ summary: "Say which instance, account and organization kcloud acts with.",
3234
3356
  usage: "whoami",
3235
3357
  options: {},
3236
3358
  positionals: {
@@ -3244,20 +3366,23 @@ const createWhoamiCommand = (adaptersOf) => defineCommand({
3244
3366
  context,
3245
3367
  api: adapters.api
3246
3368
  });
3247
- const me = dataOf("say who the token belongs to", await session.api.GET("/auth/me"));
3369
+ const me = await session.me();
3370
+ const actsIn = await session.organization();
3248
3371
  if (settings.json) {
3249
3372
  context.terminal.json({
3250
3373
  origin: session.origin,
3251
- profile: session.profileName,
3252
- ...me
3374
+ ...me,
3375
+ actsIn
3253
3376
  });
3254
3377
  return exitCodes.ok;
3255
3378
  }
3256
3379
  context.terminal.line(renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
3257
3380
  ["instance", session.origin],
3258
- ["profile", session.profileName ?? "the environment"],
3259
3381
  ["email", me.email],
3260
- ["role", me.role],
3382
+ ["organization", organizationNameOf(me.organization)],
3383
+ ["role", me.organizationRole],
3384
+ ["platform admin", me.platformAdmin ? "yes" : "no"],
3385
+ ["acts in", organizationNameOf(actsIn)],
3261
3386
  ["locale", me.locale]
3262
3387
  ]));
3263
3388
  return exitCodes.ok;
@@ -3274,6 +3399,9 @@ const runOf = async (adaptersOf, { settings, context }) => {
3274
3399
  });
3275
3400
  return {
3276
3401
  api: session.api,
3402
+ me: session.me,
3403
+ organizations: session.organizations,
3404
+ organization: session.organization,
3277
3405
  origin: session.origin,
3278
3406
  adapters,
3279
3407
  prompt: adapters.prompter(settings),
@@ -3282,40 +3410,6 @@ const runOf = async (adaptersOf, { settings, context }) => {
3282
3410
  };
3283
3411
  };
3284
3412
  //#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
3413
  //#region src/commands/agents/shared.ts
3320
3414
  const agentOption = { project: { type: "string" } };
3321
3415
  const agentUsage = [["--project <name>", "the project of the agent, when the agent is named alone"]];
@@ -3328,7 +3422,7 @@ const agentHints = {
3328
3422
  };
3329
3423
  const agentFrom = async (run, reference, project) => {
3330
3424
  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);
3425
+ return resolveAgentId(run, reference, project);
3332
3426
  };
3333
3427
  //#endregion
3334
3428
  //#region src/commands/agents/check.ts
@@ -3420,17 +3514,17 @@ const agentPlanOf = (agent) => ({
3420
3514
  });
3421
3515
  //#endregion
3422
3516
  //#region src/commands/agents/config.ts
3423
- const agentsOf = async (api, project) => {
3424
- const projects = project === void 0 ? (await projectsOf(api)).map(({ id, slug }) => ({
3517
+ const agentsOf = async (scope, project) => {
3518
+ const projects = project === void 0 ? (await projectsOf(scope)).map(({ id, slug }) => ({
3425
3519
  id,
3426
3520
  slug
3427
3521
  })) : [{
3428
- id: await resolveProjectId(api, project),
3522
+ id: await resolveProjectId(scope, project),
3429
3523
  slug: project
3430
3524
  }];
3431
3525
  const listed = [];
3432
3526
  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);
3527
+ const { items } = dataOf("list the agents of the project", await scope.api.GET("/projects/{id}/agents", { params: { path: { id } } }), agentHints);
3434
3528
  for (const agent of items) listed.push({
3435
3529
  project: slug,
3436
3530
  ...agent
@@ -3463,7 +3557,7 @@ const configCommands = (adaptersOf) => [
3463
3557
  },
3464
3558
  run: async (invocation) => {
3465
3559
  const run = await runOf(adaptersOf, invocation);
3466
- const listed = await agentsOf(run.api, invocation.values.project);
3560
+ const listed = await agentsOf(run, invocation.values.project);
3467
3561
  return printed(run, listed, (items) => items.length === 0 ? "There is no agent here yet; kcloud agents create makes one." : renderTable([
3468
3562
  { header: "PROJECT" },
3469
3563
  { header: "AGENT" },
@@ -3789,7 +3883,7 @@ const createCommand = (adaptersOf) => defineCommand({
3789
3883
  const { values } = invocation;
3790
3884
  const run = await runOf(adaptersOf, invocation);
3791
3885
  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);
3886
+ const projectId = await resolveProjectId(run, values.project);
3793
3887
  const body = values.file === void 0 ? await askedPlan(run, projectId, values) : await fileOf(run, values.file);
3794
3888
  const agent = dataOf("create the agent", await run.api.POST("/projects/{id}/agents", {
3795
3889
  params: { path: { id: projectId } },
@@ -3897,10 +3991,10 @@ const pageOptions = {
3897
3991
  };
3898
3992
  const allUsage = ["--all", "follow the cursor to the end instead of one page"];
3899
3993
  const pageUsage = [["--limit <n>", `how many rows (default: 50)`], allUsage];
3900
- const dayOf = (date) => date.toISOString().slice(0, 10);
3994
+ const dayOf$1 = (date) => date.toISOString().slice(0, 10);
3901
3995
  const costWindowOf = (days, now) => ({
3902
- from: dayOf(/* @__PURE__ */ new Date(now.getTime() - (days - 1) * 24 * 60 * 6e4)),
3903
- to: dayOf(now)
3996
+ from: dayOf$1(/* @__PURE__ */ new Date(now.getTime() - (days - 1) * 24 * 60 * 6e4)),
3997
+ to: dayOf$1(now)
3904
3998
  });
3905
3999
  const windowFrom = (days, now) => {
3906
4000
  const span = days === void 0 ? 7 : Number(days);
@@ -4334,17 +4428,6 @@ const streamFrames = async (request) => {
4334
4428
  }
4335
4429
  };
4336
4430
  //#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
4431
  //#region src/commands/agents/streams.ts
4349
4432
  const streamOptions = {
4350
4433
  ...agentOption,
@@ -4397,15 +4480,12 @@ const streamCommands = (adaptersOf) => [{
4397
4480
  api: adapters.api
4398
4481
  });
4399
4482
  const reference = positionals[0] ?? "";
4400
- const agentId = await resolveAgentId(session.api, reference, values.project);
4483
+ const agentId = await resolveAgentId(session, reference, values.project);
4401
4484
  const limit = values.limit;
4402
4485
  if (limit !== void 0 && !/^[0-9]+$/u.test(limit)) throw new UsageError(`--limit ${limit} is not a whole number.`);
4403
4486
  await streamFrames({
4404
4487
  origin: session.origin,
4405
- token: await tokenOf({
4406
- settings,
4407
- context
4408
- }),
4488
+ token: session.token,
4409
4489
  path: path.replace("{id}", encodeURIComponent(agentId)),
4410
4490
  query: {
4411
4491
  ...values.since === void 0 ? {} : { since: values.since },
@@ -4601,7 +4681,7 @@ const pipedText = async (stream) => {
4601
4681
  for await (const chunk of stream) chunks.push(Buffer.from(chunk));
4602
4682
  return Buffer.concat(chunks).toString("utf8");
4603
4683
  };
4604
- const secretOf = async ({ run, file, fromStdin = false, question, variable = secretVariable }) => {
4684
+ 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
4685
  if (file !== void 0) {
4606
4686
  const path = resolve(run.context.workingDirectory, file);
4607
4687
  try {
@@ -4611,7 +4691,7 @@ const secretOf = async ({ run, file, fromStdin = false, question, variable = sec
4611
4691
  }
4612
4692
  }
4613
4693
  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.");
4694
+ if (isTerminal(run.context.io.stdin)) throw new UsageError("Nothing is piped on stdin.", stdinHint);
4615
4695
  return (await pipedText(run.context.io.stdin)).trim();
4616
4696
  }
4617
4697
  const fromEnvironment = run.context.environment[variable];
@@ -4646,7 +4726,7 @@ const credentialHints = {
4646
4726
  };
4647
4727
  const projectFrom$2 = async (run, given) => {
4648
4728
  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);
4729
+ return resolveProjectId(run, given);
4650
4730
  };
4651
4731
  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
4732
  const resolveCredentialId = async (api, projectId, reference) => {
@@ -5020,17 +5100,18 @@ const accountTools = [
5020
5100
  defineTool({
5021
5101
  name: "whoami",
5022
5102
  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.",
5103
+ description: "Say which instance, account and organization this server acts with, and whether the version of kcloud matches the version of the instance.",
5024
5104
  operations: ["getMe", "getPlatformVersion"],
5025
5105
  safety: "read",
5026
5106
  input: z.object({}),
5027
5107
  run: async (_, run) => {
5028
- const me = dataOf("say who the token belongs to", await run.api.GET("/auth/me"));
5108
+ const user = await run.me();
5109
+ const actsIn = await run.organization();
5029
5110
  const platform = dataOf("say which version it runs", await run.api.GET("/version"));
5030
5111
  return {
5031
5112
  origin: run.origin,
5032
- profile: run.profileName,
5033
- user: me,
5113
+ user,
5114
+ actsIn,
5034
5115
  kcloud: run.kcloudVersion,
5035
5116
  instance: platform.version,
5036
5117
  matched: platform.version === run.kcloudVersion
@@ -5084,8 +5165,8 @@ const windowOfPage = ({ limit, all }, fallback = 50) => ({
5084
5165
  limit: limit ?? fallback,
5085
5166
  all
5086
5167
  });
5087
- const projectIdOf = (run, project) => resolveProjectId(run.api, project);
5088
- const agentIdOf = (run, { agent, project }) => resolveAgentId(run.api, agent, project);
5168
+ const projectIdOf = (run, project) => resolveProjectId(run, project);
5169
+ const agentIdOf = (run, { agent, project }) => resolveAgentId(run, agent, project);
5089
5170
  //#endregion
5090
5171
  //#region src/mcp/tools/agents.ts
5091
5172
  const actionOn = async (run, agentId, action, force) => dataOf(`${action} the agent`, await run.api.POST("/agents/{id}/actions/{action}", {
@@ -5103,7 +5184,7 @@ const agentTools = [
5103
5184
  operations: ["listAgents"],
5104
5185
  safety: "read",
5105
5186
  input: z.object({ project: projectField.optional().describe("One project instead of all of them.") }),
5106
- run: async ({ project }, run) => agentsOf(run.api, project)
5187
+ run: async ({ project }, run) => agentsOf(run, project)
5107
5188
  }),
5108
5189
  defineTool({
5109
5190
  name: "agent_show",
@@ -5391,10 +5472,7 @@ const createNotificationsCommand = (adaptersOf) => defineGroup({
5391
5472
  });
5392
5473
  await streamFrames({
5393
5474
  origin: session.origin,
5394
- token: await tokenOf({
5395
- settings,
5396
- context
5397
- }),
5475
+ token: session.token,
5398
5476
  path: "/notifications/stream",
5399
5477
  follow: true,
5400
5478
  action: "follow the notifications",
@@ -5649,150 +5727,685 @@ const observabilityTools = [
5649
5727
  })
5650
5728
  ];
5651
5729
  //#endregion
5652
- //#region src/commands/tools.ts
5653
- const projectOption$1 = { project: { type: "string" } };
5654
- const projectUsage$1 = [["--project <name>", "the project the tool belongs to, by slug, name or id"]];
5655
- const toolHints = {
5656
- "project.not_found": "kcloud projects list shows the projects you are a member of.",
5657
- "tool.not_found": "kcloud tools list --project <name> shows them.",
5658
- "tool.name_taken": "Another tool of the project has that name.",
5659
- "tool.slug_taken": "Another tool of the project has that slug.",
5660
- "tool.in_use": "Take it off its agents first.",
5661
- "tool.incomplete": "Every secret of the tool needs a value.",
5662
- "concurrency.conflict": "Someone changed the tool meanwhile; read it again and retry."
5663
- };
5664
- const projectFrom$1 = async (run, given) => {
5665
- 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);
5730
+ //#region src/commands/roles.ts
5731
+ const roleOption = { role: { type: "string" } };
5732
+ const roleUsage = ["--role <role>", "admin or member"];
5733
+ const roleFrom = (given, command) => {
5734
+ if (given === void 0) throw new UsageError(`${command} needs --role admin or --role member.`);
5735
+ const parsed = organizationRoleSchema.safeParse(given);
5736
+ if (!parsed.success) throw new UsageError(`--role ${given} is not a role.`, "The roles are admin and member.");
5737
+ return parsed.data;
5667
5738
  };
5668
- 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
- const resolveTool = async (api, projectId, reference) => {
5670
- const tools = await toolsOf(api, projectId);
5671
- const found = tools.filter((tool) => tool.id === reference || tool.slug === reference || tool.name === reference);
5672
- const [first] = found;
5673
- if (first === void 0) throw new CliError(`The project has no tool ${reference}.`, tools.length === 0 ? "kcloud tools create --file <path> makes one." : `Its tools are ${tools.map(({ slug }) => slug).join(", ")}.`);
5674
- if (found.length > 1) throw new CliError(`${reference} names ${found.length} tools of the project.`, "Name it by its slug or its id instead.");
5675
- return first;
5739
+ const asRole = (role) => role === "admin" ? "an admin" : "a member";
5740
+ //#endregion
5741
+ //#region src/commands/orgs/members.ts
5742
+ const memberHints = {
5743
+ ...organizationHints,
5744
+ "user.not_found": "kcloud orgs members list shows them.",
5745
+ "user.email_taken": "An account with that e-mail exists already.",
5746
+ "user.cannot_modify_self": "Another admin of the organization has to make that change.",
5747
+ "user.last_admin": "Make another member an admin first.",
5748
+ "auth.forbidden": "Changing the members of an organization takes an admin of it."
5676
5749
  };
5677
- const changedTool = async (api, projectId, tool, document) => {
5678
- const change = typeof document === "object" && document !== null && "version" in document ? document : {
5679
- ...document,
5680
- version: tool.version
5750
+ const passwordLength = `between ${passwordSchema.minLength ?? 0} and ${passwordSchema.maxLength ?? 0} characters`;
5751
+ const memberPath = (organizationId, userId) => ({ params: { path: {
5752
+ organizationId,
5753
+ userId
5754
+ } } });
5755
+ const memberRoleChanged = async (api, organizationId, userId, role) => dataOf("change the role of the member", await api.PATCH("/organizations/{organizationId}/members/{userId}", {
5756
+ ...memberPath(organizationId, userId),
5757
+ body: { organizationRole: role }
5758
+ }), memberHints);
5759
+ const memberDisabled = async (api, organizationId, userId) => dataOf("disable the member", await api.POST("/organizations/{organizationId}/members/{userId}/disable", memberPath(organizationId, userId)), memberHints);
5760
+ const memberEnabled = async (api, organizationId, userId) => dataOf("enable the member", await api.POST("/organizations/{organizationId}/members/{userId}/enable", memberPath(organizationId, userId)), memberHints);
5761
+ 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);
5762
+ const chosenMember$1 = async (run, reference) => {
5763
+ const organization = await run.organization();
5764
+ return {
5765
+ organization,
5766
+ member: await resolveOrganizationMember(run.api, organization.id, reference)
5681
5767
  };
5682
- validated(updateToolBodySchema, change, "a change to a tool");
5683
- return dataOf("change the tool", await api.PATCH("/projects/{id}/tools/{toolId}", {
5684
- params: { path: {
5685
- id: projectId,
5686
- toolId: tool.id
5687
- } },
5688
- body: change
5689
- }), toolHints);
5690
5768
  };
5691
- const createToolsCommand = (adaptersOf) => defineGroup({
5692
- name: "tools",
5693
- summary: "The HTTP APIs and MCP servers a project offers its agents.",
5694
- usage: "tools <command>",
5769
+ const localeFrom = (given) => {
5770
+ if (given === void 0) return "en";
5771
+ const parsed = localeSchema.safeParse(given);
5772
+ if (!parsed.success) throw new UsageError(`--locale ${given} is not a language of the panel.`, "The languages are en and pl.");
5773
+ return parsed.data;
5774
+ };
5775
+ const initialPasswordOf = async (run, source) => {
5776
+ const file = source["initial-password-file"];
5777
+ const fromStdin = source["initial-password-stdin"];
5778
+ if (file === void 0 && !fromStdin) return null;
5779
+ const password = await secretOf({
5780
+ run,
5781
+ file,
5782
+ fromStdin,
5783
+ question: "The initial password",
5784
+ stdinHint: "Pipe the password in, or drop --initial-password-stdin."
5785
+ });
5786
+ if (!passwordSchema.safeParse(password).success) throw new UsageError(`The initial password is not ${passwordLength}.`);
5787
+ return password;
5788
+ };
5789
+ const printedPassword = (run, password, note) => {
5790
+ run.context.terminal.line(password);
5791
+ run.context.terminal.error(note);
5792
+ return exitCodes.ok;
5793
+ };
5794
+ const memberUsage = [["", "the member is named by e-mail or id"]];
5795
+ const organizationMembersCommand = (adaptersOf) => defineGroup({
5796
+ name: "members",
5797
+ summary: "The people of the organization kcloud acts in.",
5798
+ usage: "orgs members <command>",
5695
5799
  subcommands: [
5696
5800
  defineCommand({
5697
5801
  name: "list",
5698
- summary: "List the tools of a project.",
5699
- usage: "tools list --project <name>",
5700
- optionsUsage: projectUsage$1,
5701
- options: projectOption$1,
5802
+ summary: "List the members of the organization.",
5803
+ usage: "orgs members list",
5804
+ options: {},
5702
5805
  positionals: {
5703
5806
  min: 0,
5704
5807
  max: 0
5705
5808
  },
5706
5809
  run: async (invocation) => {
5707
5810
  const run = await runOf(adaptersOf, invocation);
5708
- const projectId = await projectFrom$1(run, invocation.values.project);
5709
- const items = await toolsOf(run.api, projectId);
5710
- return printed(run, items, (tools) => tools.length === 0 ? "The project offers no tool yet." : renderTable([
5711
- { header: "SLUG" },
5712
- { header: "NAME" },
5713
- { header: "KIND" },
5714
- { header: "SECRETS" },
5715
- {
5716
- header: "AGENTS",
5717
- align: "right"
5718
- },
5811
+ const { id } = await run.organization();
5812
+ const items = await organizationMembersOf(run.api, id);
5813
+ return printed(run, items, (members) => renderTable([
5814
+ { header: "EMAIL" },
5815
+ { header: "ROLE" },
5816
+ { header: "STATUS" },
5817
+ { header: "LAST SIGN-IN" },
5719
5818
  { header: "ID" }
5720
- ], tools.map((tool) => [
5721
- tool.slug,
5722
- tool.name,
5723
- tool.kind,
5724
- tool.complete ? "complete" : "incomplete",
5725
- String(tool.agentsUsing),
5726
- tool.id
5819
+ ], members.map((member) => [
5820
+ member.email,
5821
+ member.platformAdmin ? `${member.organizationRole}, platform admin` : member.organizationRole,
5822
+ member.status,
5823
+ member.lastLoginAt ?? "never",
5824
+ member.id
5727
5825
  ])));
5728
5826
  }
5729
5827
  }),
5730
5828
  defineCommand({
5731
- name: "show",
5732
- summary: "Show one tool with its configuration.",
5733
- usage: "tools show <tool> --project <name>",
5734
- optionsUsage: projectUsage$1,
5735
- options: projectOption$1,
5736
- positionals: {
5737
- min: 1,
5738
- max: 1
5739
- },
5740
- run: async (invocation) => {
5741
- const run = await runOf(adaptersOf, invocation);
5742
- const projectId = await projectFrom$1(run, invocation.values.project);
5743
- const tool = await resolveTool(run.api, projectId, invocation.positionals[0] ?? "");
5744
- return printed(run, tool, (shown) => [
5745
- renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
5746
- ["name", shown.name],
5747
- ["slug", shown.slug],
5748
- ["kind", shown.kind],
5749
- ["version", String(shown.version)],
5750
- ["complete", shown.complete ? "yes" : "no"],
5751
- ["agents", String(shown.agentsUsing)],
5752
- ["id", shown.id]
5753
- ]),
5754
- "",
5755
- JSON.stringify(shown.config, null, 2)
5756
- ].join("\n"));
5757
- }
5758
- }),
5759
- defineCommand({
5760
- name: "create",
5761
- summary: "Create a tool from a JSON description of it.",
5762
- usage: "tools create --project <name> --file <path>",
5763
- optionsUsage: [...projectUsage$1, ...fileUsage("the tool")],
5829
+ name: "add",
5830
+ summary: "Add a person to the organization, printing a temporary password once.",
5831
+ usage: "orgs members add --email <email> --role <role> [--locale <locale>] [--initial-password-file <path> | --initial-password-stdin]",
5832
+ optionsUsage: [
5833
+ ["--email <email>", "the e-mail the person signs in with"],
5834
+ roleUsage,
5835
+ ["--locale <locale>", "the language of the panel for them, en or pl (default: en)"],
5836
+ ["--initial-password-file <path>", "read the first password from a file"],
5837
+ ["--initial-password-stdin", "read the first password from stdin"],
5838
+ ["", "a password is never taken from a flag"],
5839
+ ["", "without one the instance makes a temporary password and kcloud prints it"]
5840
+ ],
5764
5841
  options: {
5765
- ...projectOption$1,
5766
- ...fileOptions
5842
+ email: { type: "string" },
5843
+ ...roleOption,
5844
+ locale: { type: "string" },
5845
+ "initial-password-file": { type: "string" },
5846
+ "initial-password-stdin": {
5847
+ type: "boolean",
5848
+ default: false
5849
+ }
5767
5850
  },
5768
5851
  positionals: {
5769
5852
  min: 0,
5770
5853
  max: 0
5771
5854
  },
5772
5855
  run: async (invocation) => {
5856
+ const { values } = invocation;
5857
+ const email = values.email;
5858
+ if (email === void 0) throw new UsageError("orgs members add needs --email <email>.");
5859
+ const role = roleFrom(values.role, "orgs members add");
5860
+ const locale = localeFrom(values.locale);
5861
+ if (values["initial-password-file"] !== void 0 && values["initial-password-stdin"]) throw new UsageError("Pass --initial-password-file or --initial-password-stdin, not both.");
5773
5862
  const run = await runOf(adaptersOf, invocation);
5774
- const projectId = await projectFrom$1(run, invocation.values.project);
5775
- const file = invocation.values.file;
5776
- if (file === void 0) throw new UsageError("tools create needs --file <path>.", "kcloud tools show <tool> --json prints the shape of a tool.");
5777
- const document = await documentOf(run, file);
5778
- validated(createToolBodySchema, document, "a tool");
5779
- const body = document;
5780
- const tool = dataOf("create the tool", await run.api.POST("/projects/{id}/tools", {
5781
- params: { path: { id: projectId } },
5863
+ const initialPassword = await initialPasswordOf(run, values);
5864
+ const body = {
5865
+ email,
5866
+ organizationRole: role,
5867
+ locale,
5868
+ ...initialPassword === null ? {} : { initialPassword }
5869
+ };
5870
+ validated(createUserBodySchema, body, "a new member");
5871
+ const organization = await run.organization();
5872
+ const created = dataOf("add the member", await run.api.POST("/organizations/{organizationId}/members", {
5873
+ params: { path: { organizationId: organization.id } },
5782
5874
  body
5783
- }), toolHints);
5784
- return printed(run, tool, (created) => `Created ${created.name} (${created.slug}) as ${created.id}.`);
5875
+ }), memberHints);
5876
+ if (run.settings.json) {
5877
+ run.context.terminal.json(created);
5878
+ return exitCodes.ok;
5879
+ }
5880
+ const added = `Added ${created.user.email} to ${organization.slug} as ${asRole(role)}.`;
5881
+ return created.temporaryPassword === null ? said(run, added) : printedPassword(run, created.temporaryPassword, `${added} The temporary password is shown only here.`);
5785
5882
  }
5786
5883
  }),
5787
5884
  defineCommand({
5788
- name: "update",
5789
- summary: "Change a tool from a JSON description of the change.",
5790
- usage: "tools update <tool> --project <name> --file <path>",
5791
- optionsUsage: [...projectUsage$1, ...fileUsage("the change")],
5792
- options: {
5793
- ...projectOption$1,
5794
- ...fileOptions
5795
- },
5885
+ name: "set-role",
5886
+ summary: "Make a member an admin of the organization, or a member again.",
5887
+ usage: "orgs members set-role <member> --role <role>",
5888
+ optionsUsage: [roleUsage, ...memberUsage],
5889
+ options: roleOption,
5890
+ positionals: {
5891
+ min: 1,
5892
+ max: 1
5893
+ },
5894
+ run: async (invocation) => {
5895
+ const role = roleFrom(invocation.values.role, "orgs members set-role");
5896
+ const run = await runOf(adaptersOf, invocation);
5897
+ const { organization, member } = await chosenMember$1(run, invocation.positionals[0] ?? "");
5898
+ const user = await memberRoleChanged(run.api, organization.id, member.id, role);
5899
+ return printed(run, user, (shown) => `${shown.email} is ${asRole(shown.organizationRole)} of ${organization.slug} now.`);
5900
+ }
5901
+ }),
5902
+ defineCommand({
5903
+ name: "disable",
5904
+ summary: "Stop a member from signing in, keeping the account.",
5905
+ usage: "orgs members disable <member>",
5906
+ optionsUsage: memberUsage,
5907
+ options: {},
5908
+ positionals: {
5909
+ min: 1,
5910
+ max: 1
5911
+ },
5912
+ run: async (invocation) => {
5913
+ const run = await runOf(adaptersOf, invocation);
5914
+ const { organization, member } = await chosenMember$1(run, invocation.positionals[0] ?? "");
5915
+ await confirmed({
5916
+ prompt: run.prompt,
5917
+ settings: run.settings,
5918
+ question: `Disable ${member.email}? The account cannot sign in until it is enabled again.`
5919
+ });
5920
+ const user = await memberDisabled(run.api, organization.id, member.id);
5921
+ return printed(run, user, (shown) => `Disabled ${shown.email}.`);
5922
+ }
5923
+ }),
5924
+ defineCommand({
5925
+ name: "enable",
5926
+ summary: "Let a disabled member sign in again.",
5927
+ usage: "orgs members enable <member>",
5928
+ optionsUsage: memberUsage,
5929
+ options: {},
5930
+ positionals: {
5931
+ min: 1,
5932
+ max: 1
5933
+ },
5934
+ run: async (invocation) => {
5935
+ const run = await runOf(adaptersOf, invocation);
5936
+ const { organization, member } = await chosenMember$1(run, invocation.positionals[0] ?? "");
5937
+ const user = await memberEnabled(run.api, organization.id, member.id);
5938
+ return printed(run, user, (shown) => `Enabled ${shown.email}.`);
5939
+ }
5940
+ }),
5941
+ defineCommand({
5942
+ name: "reset-password",
5943
+ summary: "Give a member a new temporary password and print it once.",
5944
+ usage: "orgs members reset-password <member>",
5945
+ optionsUsage: memberUsage,
5946
+ options: {},
5947
+ positionals: {
5948
+ min: 1,
5949
+ max: 1
5950
+ },
5951
+ run: async (invocation) => {
5952
+ const run = await runOf(adaptersOf, invocation);
5953
+ const { organization, member } = await chosenMember$1(run, invocation.positionals[0] ?? "");
5954
+ await confirmed({
5955
+ prompt: run.prompt,
5956
+ settings: run.settings,
5957
+ question: `Reset the password of ${member.email}? The current one stops working.`
5958
+ });
5959
+ const reset = await passwordReset(run.api, organization.id, member.id);
5960
+ if (run.settings.json) {
5961
+ run.context.terminal.json(reset);
5962
+ return exitCodes.ok;
5963
+ }
5964
+ return printedPassword(run, reset.temporaryPassword, `The temporary password of ${member.email} is shown only here.`);
5965
+ }
5966
+ })
5967
+ ]
5968
+ });
5969
+ //#endregion
5970
+ //#region src/commands/orgs/index.ts
5971
+ const organizationOrInUse = async (scope, reference) => reference === void 0 ? scope.organization() : organizationIn(await scope.organizations(), reference);
5972
+ const organizationsMarked = async (scope, warn) => {
5973
+ const organizations = await scope.organizations();
5974
+ const inUse = await scope.organization().then(({ id }) => id, (error) => {
5975
+ if (!(error instanceof CliError)) throw error;
5976
+ warn(error);
5977
+ return null;
5978
+ });
5979
+ return organizations.map((organization) => ({
5980
+ ...organization,
5981
+ inUse: organization.id === inUse
5982
+ }));
5983
+ };
5984
+ const organizationCostsOf = async (api, organizationId, { from, to }) => dataOf("show what the organization spent", await api.GET("/organizations/{organizationId}/costs", { params: {
5985
+ path: { organizationId },
5986
+ query: {
5987
+ from,
5988
+ to
5989
+ }
5990
+ } }), organizationHints);
5991
+ const dayOf = (flag, given) => {
5992
+ if (!z.iso.date().safeParse(given).success) throw new UsageError(`--${flag} ${given} is not a day written as YYYY-MM-DD.`);
5993
+ return given;
5994
+ };
5995
+ const shiftedDay = (day, days) => new Date(Date.parse(`${day}T00:00:00.000Z`) + days * 24 * 60 * 6e4).toISOString().slice(0, 10);
5996
+ const costWindowFrom = (from, to, now) => {
5997
+ const last = to === void 0 ? now.toISOString().slice(0, 10) : dayOf("to", to);
5998
+ const first = from === void 0 ? shiftedDay(last, -6) : dayOf("from", from);
5999
+ if (first > last) throw new UsageError(`--from ${first} comes after --to ${last}.`);
6000
+ return {
6001
+ from: first,
6002
+ to: last
6003
+ };
6004
+ };
6005
+ const costsText = (costs, { from, to }, slugs) => [
6006
+ costs.items.length === 0 ? `The organization spent nothing between ${from} and ${to}.` : renderTable([
6007
+ { header: "DAY" },
6008
+ {
6009
+ header: "COST",
6010
+ align: "right"
6011
+ },
6012
+ {
6013
+ header: "RUNS",
6014
+ align: "right"
6015
+ }
6016
+ ], costs.items.map((point) => [
6017
+ point.day,
6018
+ point.costUsd.toFixed(2),
6019
+ String(point.runs)
6020
+ ])),
6021
+ ...costs.projects.length === 0 ? [] : ["", renderTable([
6022
+ { header: "PROJECT" },
6023
+ {
6024
+ header: "COST",
6025
+ align: "right"
6026
+ },
6027
+ {
6028
+ header: "RUNS",
6029
+ align: "right"
6030
+ }
6031
+ ], costs.projects.map((project) => [
6032
+ slugs.get(project.projectId) ?? project.projectId,
6033
+ project.costUsd.toFixed(2),
6034
+ String(project.runs)
6035
+ ]))],
6036
+ "",
6037
+ `${costs.totalCostUsd.toFixed(2)} over ${costs.totalRuns} runs and ${costs.sessions} sessions.`
6038
+ ].join("\n");
6039
+ const createOrgsCommand = (adaptersOf) => defineGroup({
6040
+ name: "orgs",
6041
+ summary: "The organizations you can see, and the one kcloud acts in.",
6042
+ usage: "orgs <command>",
6043
+ subcommands: [
6044
+ defineCommand({
6045
+ name: "list",
6046
+ summary: "List the organizations you can see, marking the one kcloud acts in.",
6047
+ usage: "orgs list",
6048
+ options: {},
6049
+ positionals: {
6050
+ min: 0,
6051
+ max: 0
6052
+ },
6053
+ run: async (invocation) => {
6054
+ const run = await runOf(adaptersOf, invocation);
6055
+ const items = await organizationsMarked(run, (problem) => {
6056
+ run.context.terminal.error(run.context.terminal.errorStyles.warning(`warning ${problem.message}`));
6057
+ });
6058
+ return printed(run, items, (listed) => listed.length === 0 ? "You can see no organization." : renderTable([
6059
+ { header: "SLUG" },
6060
+ { header: "NAME" },
6061
+ { header: "ID" }
6062
+ ], listed.map((organization) => [
6063
+ organization.inUse ? `${organization.slug} *` : organization.slug,
6064
+ organization.name,
6065
+ organization.id
6066
+ ])));
6067
+ }
6068
+ }),
6069
+ defineCommand({
6070
+ name: "use",
6071
+ summary: "Act in an organization on this instance from now on.",
6072
+ usage: "orgs use <organization> | orgs use --clear",
6073
+ optionsUsage: [["--clear", "act in your own organization again"], ["", `--org and ${organizationVariable} still win over the one stored here`]],
6074
+ options: { clear: {
6075
+ type: "boolean",
6076
+ default: false
6077
+ } },
6078
+ positionals: {
6079
+ min: 0,
6080
+ max: 1
6081
+ },
6082
+ run: async ({ values, positionals, settings, context }) => {
6083
+ const [reference] = positionals;
6084
+ 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.");
6085
+ const adapters = adaptersOf(context);
6086
+ const session = await openSession({
6087
+ settings,
6088
+ context,
6089
+ api: adapters.api
6090
+ });
6091
+ 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.`);
6092
+ const organization = reference === void 0 ? null : organizationIn(await session.organizations(), reference);
6093
+ await writeInstanceStore(context.homeDirectory, withInstance(session.store, session.origin, {
6094
+ ...session.stored,
6095
+ organization: organization?.slug ?? null
6096
+ }));
6097
+ if (givenIn(context.environment, "KIKIMORA_CLOUD_ORG") !== null) context.terminal.error(context.terminal.errorStyles.warning(`warning ${organizationVariable} still wins over it in this environment.`));
6098
+ return printed({
6099
+ settings,
6100
+ context
6101
+ }, {
6102
+ origin: session.origin,
6103
+ organization
6104
+ }, ({ origin }) => organization === null ? `kcloud acts in your own organization on ${origin} again.` : `kcloud acts in ${organizationNameOf(organization)} on ${origin} from now on.`);
6105
+ }
6106
+ }),
6107
+ defineCommand({
6108
+ name: "show",
6109
+ summary: "Show an organization, by default the one kcloud acts in.",
6110
+ usage: "orgs show [<organization>]",
6111
+ options: {},
6112
+ positionals: {
6113
+ min: 0,
6114
+ max: 1
6115
+ },
6116
+ run: async (invocation) => {
6117
+ const run = await runOf(adaptersOf, invocation);
6118
+ const { id } = await organizationOrInUse(run, invocation.positionals[0]);
6119
+ const organization = dataOf("describe the organization", await run.api.GET("/organizations/{organizationId}", { params: { path: { organizationId: id } } }), organizationHints);
6120
+ return printed(run, organization, (shown) => renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
6121
+ ["name", shown.name],
6122
+ ["slug", shown.slug],
6123
+ ["created", shown.createdAt],
6124
+ ["id", shown.id]
6125
+ ]));
6126
+ }
6127
+ }),
6128
+ defineCommand({
6129
+ name: "costs",
6130
+ summary: "Show what the organization spent, by day and by project.",
6131
+ usage: "orgs costs [--from <day>] [--to <day>]",
6132
+ 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)"]],
6133
+ options: {
6134
+ from: { type: "string" },
6135
+ to: { type: "string" }
6136
+ },
6137
+ positionals: {
6138
+ min: 0,
6139
+ max: 0
6140
+ },
6141
+ run: async (invocation) => {
6142
+ const { values } = invocation;
6143
+ const run = await runOf(adaptersOf, invocation);
6144
+ const window = costWindowFrom(values.from, values.to, run.adapters.now());
6145
+ const organization = await run.organization();
6146
+ const costs = await organizationCostsOf(run.api, organization.id, window);
6147
+ if (run.settings.json) {
6148
+ run.context.terminal.json(costs);
6149
+ return exitCodes.ok;
6150
+ }
6151
+ const slugs = new Map(costs.projects.length === 0 ? [] : (await projectsOf(run)).map(({ id, slug }) => [id, slug]));
6152
+ run.context.terminal.line(costsText(costs, window, slugs));
6153
+ return exitCodes.ok;
6154
+ }
6155
+ }),
6156
+ organizationMembersCommand(adaptersOf)
6157
+ ]
6158
+ });
6159
+ //#endregion
6160
+ //#region src/mcp/tools/organizations.ts
6161
+ const memberField = z.string().trim().min(1).describe("The member, by e-mail or id, from organization_members_list.");
6162
+ const dayField = z.iso.date();
6163
+ const chosenMember = async (run, member) => {
6164
+ const { id } = await run.organization();
6165
+ return {
6166
+ organizationId: id,
6167
+ userId: (await resolveOrganizationMember(run.api, id, member)).id
6168
+ };
6169
+ };
6170
+ const organizationTools = [
6171
+ defineTool({
6172
+ name: "organizations_list",
6173
+ title: "List organizations",
6174
+ description: "List the organizations you can see, marking the one this server acts in. A member sees one, a platform admin sees all.",
6175
+ operations: ["listOrganizations"],
6176
+ safety: "read",
6177
+ input: z.object({}),
6178
+ run: (_, run) => organizationsMarked(run, () => void 0)
6179
+ }),
6180
+ defineTool({
6181
+ name: "organization_show",
6182
+ title: "Show an organization",
6183
+ description: "Show an organization, by default the one this server acts in.",
6184
+ operations: ["getOrganization"],
6185
+ safety: "read",
6186
+ input: z.object({ organization: z.string().trim().min(1).optional().describe("The organization, by its slug, its name or its id.") }),
6187
+ run: async ({ organization }, run) => {
6188
+ const { id } = await organizationOrInUse(run, organization);
6189
+ return dataOf("describe the organization", await run.api.GET("/organizations/{organizationId}", { params: { path: { organizationId: id } } }), organizationHints);
6190
+ }
6191
+ }),
6192
+ defineTool({
6193
+ name: "organization_costs",
6194
+ title: "Show what the organization spent",
6195
+ 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.",
6196
+ operations: ["getOrganizationCosts"],
6197
+ safety: "read",
6198
+ input: z.object({
6199
+ from: dayField.optional().describe("The first day, as YYYY-MM-DD."),
6200
+ to: dayField.optional().describe("The last day, as YYYY-MM-DD, today by default.")
6201
+ }),
6202
+ run: async ({ from, to }, run) => {
6203
+ const { id } = await run.organization();
6204
+ return organizationCostsOf(run.api, id, costWindowFrom(from, to, run.now()));
6205
+ }
6206
+ }),
6207
+ defineTool({
6208
+ name: "organization_members_list",
6209
+ title: "List the members of the organization",
6210
+ description: "List the people of the organization this server acts in, with their role and status.",
6211
+ operations: ["listOrganizationMembers"],
6212
+ safety: "read",
6213
+ input: z.object({}),
6214
+ run: async (_, run) => organizationMembersOf(run.api, (await run.organization()).id)
6215
+ }),
6216
+ defineTool({
6217
+ name: "organization_member_set_role",
6218
+ title: "Set the role of a member",
6219
+ description: "Make a member an admin of the organization, or a member again. It takes an admin of the organization.",
6220
+ operations: ["updateOrganizationMember"],
6221
+ safety: "write",
6222
+ idempotent: true,
6223
+ input: z.object({
6224
+ member: memberField,
6225
+ role: organizationRoleSchema
6226
+ }),
6227
+ run: async ({ member, role }, run) => {
6228
+ const { organizationId, userId } = await chosenMember(run, member);
6229
+ return memberRoleChanged(run.api, organizationId, userId, role);
6230
+ }
6231
+ }),
6232
+ defineTool({
6233
+ name: "organization_member_disable",
6234
+ title: "Disable a member",
6235
+ description: "Stop a member from signing in, keeping the account. It takes an admin of the organization.",
6236
+ operations: ["disableOrganizationMember"],
6237
+ safety: "destructive",
6238
+ input: z.object({ member: memberField }),
6239
+ run: async ({ member }, run) => {
6240
+ const { organizationId, userId } = await chosenMember(run, member);
6241
+ return memberDisabled(run.api, organizationId, userId);
6242
+ }
6243
+ }),
6244
+ defineTool({
6245
+ name: "organization_member_enable",
6246
+ title: "Enable a member",
6247
+ description: "Let a disabled member sign in again. It takes an admin of the organization.",
6248
+ operations: ["enableOrganizationMember"],
6249
+ safety: "write",
6250
+ input: z.object({ member: memberField }),
6251
+ run: async ({ member }, run) => {
6252
+ const { organizationId, userId } = await chosenMember(run, member);
6253
+ return memberEnabled(run.api, organizationId, userId);
6254
+ }
6255
+ })
6256
+ ];
6257
+ //#endregion
6258
+ //#region src/commands/tools.ts
6259
+ const projectOption$1 = { project: { type: "string" } };
6260
+ const projectUsage$1 = [["--project <name>", "the project the tool belongs to, by slug, name or id"]];
6261
+ const toolHints = {
6262
+ "project.not_found": "kcloud projects list shows the projects you are a member of.",
6263
+ "tool.not_found": "kcloud tools list --project <name> shows them.",
6264
+ "tool.name_taken": "Another tool of the project has that name.",
6265
+ "tool.slug_taken": "Another tool of the project has that slug.",
6266
+ "tool.in_use": "Take it off its agents first.",
6267
+ "tool.incomplete": "Every secret of the tool needs a value.",
6268
+ "concurrency.conflict": "Someone changed the tool meanwhile; read it again and retry."
6269
+ };
6270
+ const projectFrom$1 = async (run, given) => {
6271
+ if (given === void 0) throw new UsageError("This command needs a project.", "Pass --project <name>; kcloud projects list shows them.");
6272
+ return resolveProjectId(run, given);
6273
+ };
6274
+ const toolsOf = async (api, projectId) => dataOf("list the tools of the project", await api.GET("/projects/{id}/tools", { params: { path: { id: projectId } } }), toolHints).items;
6275
+ const resolveTool = async (api, projectId, reference) => {
6276
+ const tools = await toolsOf(api, projectId);
6277
+ const found = tools.filter((tool) => tool.id === reference || tool.slug === reference || tool.name === reference);
6278
+ const [first] = found;
6279
+ if (first === void 0) throw new CliError(`The project has no tool ${reference}.`, tools.length === 0 ? "kcloud tools create --file <path> makes one." : `Its tools are ${tools.map(({ slug }) => slug).join(", ")}.`);
6280
+ if (found.length > 1) throw new CliError(`${reference} names ${found.length} tools of the project.`, "Name it by its slug or its id instead.");
6281
+ return first;
6282
+ };
6283
+ const changedTool = async (api, projectId, tool, document) => {
6284
+ const change = typeof document === "object" && document !== null && "version" in document ? document : {
6285
+ ...document,
6286
+ version: tool.version
6287
+ };
6288
+ validated(updateToolBodySchema, change, "a change to a tool");
6289
+ return dataOf("change the tool", await api.PATCH("/projects/{id}/tools/{toolId}", {
6290
+ params: { path: {
6291
+ id: projectId,
6292
+ toolId: tool.id
6293
+ } },
6294
+ body: change
6295
+ }), toolHints);
6296
+ };
6297
+ const copiedTool = async (api, projectId, toolId, targetProjectId) => dataOf("copy the tool", await api.POST("/projects/{id}/tools/{toolId}/copy", {
6298
+ params: { path: {
6299
+ id: projectId,
6300
+ toolId
6301
+ } },
6302
+ body: { targetProjectId }
6303
+ }), toolHints);
6304
+ const createToolsCommand = (adaptersOf) => defineGroup({
6305
+ name: "tools",
6306
+ summary: "The HTTP APIs and MCP servers a project offers its agents.",
6307
+ usage: "tools <command>",
6308
+ subcommands: [
6309
+ defineCommand({
6310
+ name: "list",
6311
+ summary: "List the tools of a project.",
6312
+ usage: "tools list --project <name>",
6313
+ optionsUsage: projectUsage$1,
6314
+ options: projectOption$1,
6315
+ positionals: {
6316
+ min: 0,
6317
+ max: 0
6318
+ },
6319
+ run: async (invocation) => {
6320
+ const run = await runOf(adaptersOf, invocation);
6321
+ const projectId = await projectFrom$1(run, invocation.values.project);
6322
+ const items = await toolsOf(run.api, projectId);
6323
+ return printed(run, items, (tools) => tools.length === 0 ? "The project offers no tool yet." : renderTable([
6324
+ { header: "SLUG" },
6325
+ { header: "NAME" },
6326
+ { header: "KIND" },
6327
+ { header: "SECRETS" },
6328
+ {
6329
+ header: "AGENTS",
6330
+ align: "right"
6331
+ },
6332
+ { header: "ID" }
6333
+ ], tools.map((tool) => [
6334
+ tool.slug,
6335
+ tool.name,
6336
+ tool.kind,
6337
+ tool.complete ? "complete" : "incomplete",
6338
+ String(tool.agentsUsing),
6339
+ tool.id
6340
+ ])));
6341
+ }
6342
+ }),
6343
+ defineCommand({
6344
+ name: "show",
6345
+ summary: "Show one tool with its configuration.",
6346
+ usage: "tools show <tool> --project <name>",
6347
+ optionsUsage: projectUsage$1,
6348
+ options: projectOption$1,
6349
+ positionals: {
6350
+ min: 1,
6351
+ max: 1
6352
+ },
6353
+ run: async (invocation) => {
6354
+ const run = await runOf(adaptersOf, invocation);
6355
+ const projectId = await projectFrom$1(run, invocation.values.project);
6356
+ const tool = await resolveTool(run.api, projectId, invocation.positionals[0] ?? "");
6357
+ return printed(run, tool, (shown) => [
6358
+ renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
6359
+ ["name", shown.name],
6360
+ ["slug", shown.slug],
6361
+ ["kind", shown.kind],
6362
+ ["version", String(shown.version)],
6363
+ ["complete", shown.complete ? "yes" : "no"],
6364
+ ["agents", String(shown.agentsUsing)],
6365
+ ["id", shown.id]
6366
+ ]),
6367
+ "",
6368
+ JSON.stringify(shown.config, null, 2)
6369
+ ].join("\n"));
6370
+ }
6371
+ }),
6372
+ defineCommand({
6373
+ name: "create",
6374
+ summary: "Create a tool from a JSON description of it.",
6375
+ usage: "tools create --project <name> --file <path>",
6376
+ optionsUsage: [...projectUsage$1, ...fileUsage("the tool")],
6377
+ options: {
6378
+ ...projectOption$1,
6379
+ ...fileOptions
6380
+ },
6381
+ positionals: {
6382
+ min: 0,
6383
+ max: 0
6384
+ },
6385
+ run: async (invocation) => {
6386
+ const run = await runOf(adaptersOf, invocation);
6387
+ const projectId = await projectFrom$1(run, invocation.values.project);
6388
+ const file = invocation.values.file;
6389
+ if (file === void 0) throw new UsageError("tools create needs --file <path>.", "kcloud tools show <tool> --json prints the shape of a tool.");
6390
+ const document = await documentOf(run, file);
6391
+ validated(createToolBodySchema, document, "a tool");
6392
+ const body = document;
6393
+ const tool = dataOf("create the tool", await run.api.POST("/projects/{id}/tools", {
6394
+ params: { path: { id: projectId } },
6395
+ body
6396
+ }), toolHints);
6397
+ return printed(run, tool, (created) => `Created ${created.name} (${created.slug}) as ${created.id}.`);
6398
+ }
6399
+ }),
6400
+ defineCommand({
6401
+ name: "update",
6402
+ summary: "Change a tool from a JSON description of the change.",
6403
+ usage: "tools update <tool> --project <name> --file <path>",
6404
+ optionsUsage: [...projectUsage$1, ...fileUsage("the change")],
6405
+ options: {
6406
+ ...projectOption$1,
6407
+ ...fileOptions
6408
+ },
5796
6409
  positionals: {
5797
6410
  min: 1,
5798
6411
  max: 1
@@ -5833,6 +6446,31 @@ const createToolsCommand = (adaptersOf) => defineGroup({
5833
6446
  } } }), toolHints);
5834
6447
  return said(run, `Deleted the tool ${tool.name}.`);
5835
6448
  }
6449
+ }),
6450
+ defineCommand({
6451
+ name: "copy",
6452
+ summary: "Copy a tool to another project, without its secrets.",
6453
+ usage: "tools copy <tool> --project <name> --to <project>",
6454
+ optionsUsage: [...projectUsage$1, ["--to <project>", "the project that receives the copy, by slug, name or id"]],
6455
+ options: {
6456
+ ...projectOption$1,
6457
+ to: { type: "string" }
6458
+ },
6459
+ positionals: {
6460
+ min: 1,
6461
+ max: 1
6462
+ },
6463
+ run: async (invocation) => {
6464
+ const { values } = invocation;
6465
+ const target = values.to;
6466
+ if (target === void 0) throw new UsageError("tools copy needs --to <project>.", "kcloud projects list shows the projects.");
6467
+ const run = await runOf(adaptersOf, invocation);
6468
+ const projectId = await projectFrom$1(run, values.project);
6469
+ const tool = await resolveTool(run.api, projectId, invocation.positionals[0] ?? "");
6470
+ const targetProjectId = await resolveProjectId(run, target);
6471
+ const copied = await copiedTool(run.api, projectId, tool.id, targetProjectId);
6472
+ 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(" "));
6473
+ }
5836
6474
  })
5837
6475
  ]
5838
6476
  });
@@ -5906,6 +6544,23 @@ const projectToolTools = [
5906
6544
  });
5907
6545
  }
5908
6546
  }),
6547
+ defineTool({
6548
+ name: "tool_copy",
6549
+ title: "Copy a tool",
6550
+ 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.",
6551
+ operations: ["copyTool"],
6552
+ safety: "write",
6553
+ input: z.object({
6554
+ project: projectField,
6555
+ tool: toolField,
6556
+ to: projectField.describe("The project that receives the copy, by slug, name or id.")
6557
+ }),
6558
+ run: async ({ project, tool, to }, run) => {
6559
+ const projectId = await projectIdOf(run, project);
6560
+ const found = await resolveTool(run.api, projectId, tool);
6561
+ return copiedTool(run.api, projectId, found.id, await projectIdOf(run, to));
6562
+ }
6563
+ }),
5909
6564
  defineTool({
5910
6565
  name: "tool_delete",
5911
6566
  title: "Delete a tool",
@@ -5928,16 +6583,178 @@ const projectToolTools = [
5928
6583
  })
5929
6584
  ];
5930
6585
  //#endregion
5931
- //#region src/commands/projects.ts
5932
- const projectHint = { "project.not_found": "kcloud projects list shows the projects you are a member of." };
6586
+ //#region src/commands/projects/shared.ts
6587
+ const defaultProjectColor = "#7c86ff";
6588
+ const projectHint = {
6589
+ "project.not_found": "kcloud projects list shows the projects you are a member of.",
6590
+ "project.slug_taken": "Another project of the organization has that slug.",
6591
+ "project.invalid_slug": "A slug holds lowercase letters, digits and hyphens.",
6592
+ "project.not_empty": "Delete its agents, credentials, repositories and tools first.",
6593
+ "project.member_not_found": "kcloud projects members list <project> shows them.",
6594
+ "project.member_must_be_active": "kcloud orgs members enable <member> enables the account first."
6595
+ };
6596
+ const projectOf = async (api, id) => dataOf("describe the project", await api.GET("/projects/{id}", { params: { path: { id } } }), projectHint);
6597
+ //#endregion
6598
+ //#region src/commands/projects/members.ts
6599
+ const projectMembersOf = async (api, projectId) => dataOf("list the members of the project", await api.GET("/projects/{id}/members", { params: { path: { id: projectId } } }), projectHint).items;
6600
+ const resolveProjectMember = async (api, projectId, reference) => {
6601
+ const found = (await projectMembersOf(api, projectId)).find((member) => member.userId === reference.toLowerCase() || sameEmail(member.email, reference));
6602
+ if (found === void 0) throw new CliError(`The project has no member ${reference}.`, "kcloud projects members list <project> shows them.");
6603
+ return found;
6604
+ };
6605
+ const projectUserIdOf = async (api, projectId, reference) => {
6606
+ if (isId(reference)) return reference.toLowerCase();
6607
+ const { organizationId } = await projectOf(api, projectId);
6608
+ return (await resolveOrganizationMember(api, organizationId, reference)).id;
6609
+ };
6610
+ const setProjectMember = async (api, projectId, userId, role) => {
6611
+ doneWith("set the member of the project", await api.PUT("/projects/{id}/members/{userId}", {
6612
+ params: { path: {
6613
+ id: projectId,
6614
+ userId
6615
+ } },
6616
+ body: { role }
6617
+ }), projectHint);
6618
+ };
6619
+ const removeProjectMember = async (api, projectId, userId) => {
6620
+ doneWith("remove the member of the project", await api.DELETE("/projects/{id}/members/{userId}", { params: { path: {
6621
+ id: projectId,
6622
+ userId
6623
+ } } }), projectHint);
6624
+ };
6625
+ const projectMembersCommand = (adaptersOf) => defineGroup({
6626
+ name: "members",
6627
+ summary: "The members of a project and their project role.",
6628
+ usage: "projects members <command>",
6629
+ subcommands: [
6630
+ defineCommand({
6631
+ name: "list",
6632
+ summary: "List the members of one project.",
6633
+ usage: "projects members list <project>",
6634
+ options: {},
6635
+ positionals: {
6636
+ min: 1,
6637
+ max: 1
6638
+ },
6639
+ run: async (invocation) => {
6640
+ const run = await runOf(adaptersOf, invocation);
6641
+ const id = await resolveProjectId(run, invocation.positionals[0] ?? "");
6642
+ const items = await projectMembersOf(run.api, id);
6643
+ return printed(run, items, (members) => members.length === 0 ? "The project has no member." : renderTable([
6644
+ { header: "EMAIL" },
6645
+ { header: "ROLE" },
6646
+ { header: "STATUS" },
6647
+ { header: "ADDED" },
6648
+ { header: "ID" }
6649
+ ], members.map((member) => [
6650
+ member.email,
6651
+ member.role,
6652
+ member.status,
6653
+ member.addedAt,
6654
+ member.userId
6655
+ ])));
6656
+ }
6657
+ }),
6658
+ defineCommand({
6659
+ name: "set",
6660
+ summary: "Add a member of the organization to a project, or change their role.",
6661
+ usage: "projects members set <project> <user> --role <role>",
6662
+ optionsUsage: [roleUsage, ["", "the user is named by e-mail or id, and belongs to the organization"]],
6663
+ options: roleOption,
6664
+ positionals: {
6665
+ min: 2,
6666
+ max: 2
6667
+ },
6668
+ run: async (invocation) => {
6669
+ const [project = "", user = ""] = invocation.positionals;
6670
+ const role = roleFrom(invocation.values.role, "projects members set");
6671
+ const run = await runOf(adaptersOf, invocation);
6672
+ const id = await resolveProjectId(run, project);
6673
+ const userId = await projectUserIdOf(run.api, id, user);
6674
+ await setProjectMember(run.api, id, userId, role);
6675
+ return said(run, `${user} is ${asRole(role)} of the project ${project} now.`);
6676
+ }
6677
+ }),
6678
+ defineCommand({
6679
+ name: "remove",
6680
+ summary: "Take a member off a project.",
6681
+ usage: "projects members remove <project> <user>",
6682
+ optionsUsage: [["", "the user is named by e-mail or id"]],
6683
+ options: {},
6684
+ positionals: {
6685
+ min: 2,
6686
+ max: 2
6687
+ },
6688
+ run: async (invocation) => {
6689
+ const [project = "", user = ""] = invocation.positionals;
6690
+ const run = await runOf(adaptersOf, invocation);
6691
+ const id = await resolveProjectId(run, project);
6692
+ const member = await resolveProjectMember(run.api, id, user);
6693
+ await confirmed({
6694
+ prompt: run.prompt,
6695
+ settings: run.settings,
6696
+ question: `Remove ${member.email} from the project ${project}?`
6697
+ });
6698
+ await removeProjectMember(run.api, id, member.userId);
6699
+ return said(run, `Removed ${member.email} from the project ${project}.`);
6700
+ }
6701
+ })
6702
+ ]
6703
+ });
6704
+ //#endregion
6705
+ //#region src/commands/projects/index.ts
6706
+ const fieldOptions = {
6707
+ slug: { type: "string" },
6708
+ description: { type: "string" },
6709
+ color: { type: "string" }
6710
+ };
6711
+ const fieldUsage = [
6712
+ ["--slug <slug>", "lowercase letters, digits and hyphens (default: made from the name)"],
6713
+ ["--description <text>", "what the project is for"],
6714
+ ["--color <#rrggbb>", `the color of the project in the panel (default: ${defaultProjectColor})`]
6715
+ ];
6716
+ const createdProject = async (scope, name, { slug, description, color }) => {
6717
+ const body = {
6718
+ name,
6719
+ ...slug === void 0 ? {} : { slug },
6720
+ description: description ?? "",
6721
+ color: color ?? "#7c86ff"
6722
+ };
6723
+ validated(createProjectBodySchema, body, "a project");
6724
+ const organization = await scope.organization();
6725
+ return {
6726
+ project: dataOf("create the project", await scope.api.POST("/organizations/{organizationId}/projects", {
6727
+ params: { path: { organizationId: organization.id } },
6728
+ body
6729
+ }), projectHint),
6730
+ organization
6731
+ };
6732
+ };
6733
+ const changedProject = async (api, id, change) => {
6734
+ const current = await projectOf(api, id);
6735
+ const body = {
6736
+ name: change.name ?? current.name,
6737
+ slug: change.slug ?? current.slug,
6738
+ description: change.description ?? current.description,
6739
+ color: change.color ?? current.color
6740
+ };
6741
+ validated(updateProjectBodySchema, body, "a change to a project");
6742
+ return dataOf("change the project", await api.PATCH("/projects/{id}", {
6743
+ params: { path: { id } },
6744
+ body
6745
+ }), projectHint);
6746
+ };
6747
+ const deleteProject = async (api, id) => {
6748
+ doneWith("delete the project", await api.DELETE("/projects/{id}", { params: { path: { id } } }), projectHint);
6749
+ };
5933
6750
  const createProjectsCommand = (adaptersOf) => defineGroup({
5934
6751
  name: "projects",
5935
- summary: "The projects you are a member of.",
6752
+ summary: "The projects of the organization you can see.",
5936
6753
  usage: "projects <command>",
5937
6754
  subcommands: [
5938
6755
  defineCommand({
5939
6756
  name: "list",
5940
- summary: "List the projects you can see.",
6757
+ summary: "List the projects of the organization you can see.",
5941
6758
  usage: "projects list",
5942
6759
  options: {},
5943
6760
  positionals: {
@@ -5946,8 +6763,9 @@ const createProjectsCommand = (adaptersOf) => defineGroup({
5946
6763
  },
5947
6764
  run: async (invocation) => {
5948
6765
  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([
6766
+ const items = await projectsOf(run);
6767
+ const { slug } = await run.organization();
6768
+ return printed(run, items, (projects) => projects.length === 0 ? `You see no project in ${slug} yet.` : renderTable([
5951
6769
  { header: "SLUG" },
5952
6770
  { header: "NAME" },
5953
6771
  {
@@ -5976,12 +6794,13 @@ const createProjectsCommand = (adaptersOf) => defineGroup({
5976
6794
  },
5977
6795
  run: async (invocation) => {
5978
6796
  const run = await runOf(adaptersOf, invocation);
5979
- const id = await resolveProjectId(run.api, invocation.positionals[0] ?? "");
6797
+ const id = await resolveProjectId(run, invocation.positionals[0] ?? "");
5980
6798
  const project = dataOf("describe the project", await run.api.GET("/projects/{id}", { params: { path: { id } } }), projectHint);
5981
6799
  return printed(run, project, (shown) => renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
5982
6800
  ["name", shown.name],
5983
6801
  ["slug", shown.slug],
5984
6802
  ["description", shown.description === "" ? "-" : shown.description],
6803
+ ["color", shown.color],
5985
6804
  ["docker", shown.dockerAllowed ? "allowed" : "not allowed"],
5986
6805
  ["members", String(shown.memberCount)],
5987
6806
  ["created", shown.createdAt],
@@ -5990,9 +6809,52 @@ const createProjectsCommand = (adaptersOf) => defineGroup({
5990
6809
  }
5991
6810
  }),
5992
6811
  defineCommand({
5993
- name: "members",
5994
- summary: "List the members of one project.",
5995
- usage: "projects members <project>",
6812
+ name: "create",
6813
+ summary: "Create a project in the organization.",
6814
+ usage: "projects create <name> [--slug <slug>] [--description <text>] [--color <#rrggbb>]",
6815
+ optionsUsage: fieldUsage,
6816
+ options: fieldOptions,
6817
+ positionals: {
6818
+ min: 1,
6819
+ max: 1
6820
+ },
6821
+ run: async (invocation) => {
6822
+ const run = await runOf(adaptersOf, invocation);
6823
+ const { project, organization } = await createdProject(run, invocation.positionals[0] ?? "", invocation.values);
6824
+ return printed(run, project, (created) => `Created ${created.name} (${created.slug}) in ${organization.slug} as ${created.id}.`);
6825
+ }
6826
+ }),
6827
+ defineCommand({
6828
+ name: "update",
6829
+ summary: "Change the name, slug, description or color of a project.",
6830
+ usage: "projects update <project> [--name <name>] [--slug <slug>] [--description <text>] [--color <#rrggbb>]",
6831
+ optionsUsage: [["--name <name>", "the new name"], ...fieldUsage],
6832
+ options: {
6833
+ name: { type: "string" },
6834
+ ...fieldOptions
6835
+ },
6836
+ positionals: {
6837
+ min: 1,
6838
+ max: 1
6839
+ },
6840
+ run: async (invocation) => {
6841
+ const { values } = invocation;
6842
+ if ([
6843
+ values.name,
6844
+ values.slug,
6845
+ values.description,
6846
+ values.color
6847
+ ].every((value) => value === void 0)) throw new UsageError("projects update needs --name, --slug, --description or --color.");
6848
+ const run = await runOf(adaptersOf, invocation);
6849
+ const id = await resolveProjectId(run, invocation.positionals[0] ?? "");
6850
+ const project = await changedProject(run.api, id, values);
6851
+ return printed(run, project, (shown) => `Changed ${shown.name} (${shown.slug}).`);
6852
+ }
6853
+ }),
6854
+ defineCommand({
6855
+ name: "delete",
6856
+ summary: "Delete a project that holds no agent, credential, repository or tool.",
6857
+ usage: "projects delete <project>",
5996
6858
  options: {},
5997
6859
  positionals: {
5998
6860
  min: 1,
@@ -6000,50 +6862,136 @@ const createProjectsCommand = (adaptersOf) => defineGroup({
6000
6862
  },
6001
6863
  run: async (invocation) => {
6002
6864
  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
- ])));
6865
+ const id = await resolveProjectId(run, invocation.positionals[0] ?? "");
6866
+ const project = await projectOf(run.api, id);
6867
+ await confirmed({
6868
+ prompt: run.prompt,
6869
+ settings: run.settings,
6870
+ question: `Delete the project ${project.name} (${project.slug})?`
6871
+ });
6872
+ await deleteProject(run.api, id);
6873
+ return said(run, `Deleted the project ${project.name}.`);
6016
6874
  }
6017
- })
6875
+ }),
6876
+ projectMembersCommand(adaptersOf)
6018
6877
  ]
6019
6878
  });
6020
6879
  //#endregion
6021
6880
  //#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
- })];
6881
+ const userField = z.string().trim().min(1).describe("The user, by e-mail or id, a member of the organization of the project.");
6882
+ const fields = {
6883
+ slug: slugSchema.optional().describe("Lowercase letters, digits and hyphens."),
6884
+ description: z.string().max(500).optional(),
6885
+ color: colorSchema.optional().describe("The color in the panel, as #rrggbb.")
6886
+ };
6887
+ const projectTools = [
6888
+ defineTool({
6889
+ name: "projects_list",
6890
+ title: "List projects",
6891
+ description: "List the projects you can see in the organization this server acts in.",
6892
+ operations: ["listProjects"],
6893
+ safety: "read",
6894
+ input: z.object({}),
6895
+ run: async (_, run) => projectsOf(run)
6896
+ }),
6897
+ defineTool({
6898
+ name: "project_show",
6899
+ title: "Show a project",
6900
+ description: "Show one project together with its members.",
6901
+ operations: ["getProject", "listProjectMembers"],
6902
+ safety: "read",
6903
+ input: z.object({ project: projectField }),
6904
+ run: async ({ project }, run) => {
6905
+ const id = await projectIdOf(run, project);
6906
+ return {
6907
+ ...dataOf("describe the project", await run.api.GET("/projects/{id}", { params: { path: { id } } }), projectHint),
6908
+ members: await projectMembersOf(run.api, id)
6909
+ };
6910
+ }
6911
+ }),
6912
+ defineTool({
6913
+ name: "project_create",
6914
+ title: "Create a project",
6915
+ description: "Create a project in the organization this server acts in.",
6916
+ operations: ["createProject"],
6917
+ safety: "write",
6918
+ input: z.object({
6919
+ name: z.string().trim().min(2).max(80),
6920
+ ...fields
6921
+ }),
6922
+ run: async ({ name, ...rest }, run) => (await createdProject(run, name, rest)).project
6923
+ }),
6924
+ defineTool({
6925
+ name: "project_update",
6926
+ title: "Change a project",
6927
+ description: "Change the name, slug, description or color of a project. A field left out keeps its value.",
6928
+ operations: ["updateProject"],
6929
+ safety: "write",
6930
+ idempotent: true,
6931
+ input: z.object({
6932
+ project: projectField,
6933
+ name: z.string().trim().min(2).max(80).optional(),
6934
+ ...fields
6935
+ }),
6936
+ run: async ({ project, ...change }, run) => changedProject(run.api, await projectIdOf(run, project), change)
6937
+ }),
6938
+ defineTool({
6939
+ name: "project_delete",
6940
+ title: "Delete a project",
6941
+ description: "Delete a project. The instance refuses while it holds agents, credentials, repositories or tools.",
6942
+ operations: ["deleteProject"],
6943
+ safety: "destructive",
6944
+ input: z.object({ project: projectField }),
6945
+ run: async ({ project }, run) => {
6946
+ const id = await projectIdOf(run, project);
6947
+ await deleteProject(run.api, id);
6948
+ return { deleted: id };
6949
+ }
6950
+ }),
6951
+ defineTool({
6952
+ name: "project_member_set",
6953
+ title: "Set a member of a project",
6954
+ description: "Add a member of the organization to a project with a project role, or change the role of a member.",
6955
+ operations: ["setProjectMember"],
6956
+ safety: "write",
6957
+ idempotent: true,
6958
+ input: z.object({
6959
+ project: projectField,
6960
+ user: userField,
6961
+ role: projectRoleSchema
6962
+ }),
6963
+ run: async ({ project, user, role }, run) => {
6964
+ const id = await projectIdOf(run, project);
6965
+ const userId = await projectUserIdOf(run.api, id, user);
6966
+ await setProjectMember(run.api, id, userId, role);
6967
+ return {
6968
+ projectId: id,
6969
+ userId,
6970
+ role
6971
+ };
6972
+ }
6973
+ }),
6974
+ defineTool({
6975
+ name: "project_member_remove",
6976
+ title: "Remove a member of a project",
6977
+ description: "Take a member off a project. The account stays in the organization.",
6978
+ operations: ["removeProjectMember"],
6979
+ safety: "destructive",
6980
+ input: z.object({
6981
+ project: projectField,
6982
+ user: userField
6983
+ }),
6984
+ run: async ({ project, user }, run) => {
6985
+ const id = await projectIdOf(run, project);
6986
+ const member = await resolveProjectMember(run.api, id, user);
6987
+ await removeProjectMember(run.api, id, member.userId);
6988
+ return {
6989
+ projectId: id,
6990
+ removed: member.userId
6991
+ };
6992
+ }
6993
+ })
6994
+ ];
6047
6995
  //#endregion
6048
6996
  //#region src/mcp/tools/releases.ts
6049
6997
  const releaseTools = [defineTool({
@@ -6071,7 +7019,7 @@ const repositoryHints = {
6071
7019
  };
6072
7020
  const projectFrom = async (run, given) => {
6073
7021
  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);
7022
+ return resolveProjectId(run, given);
6075
7023
  };
6076
7024
  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
7025
  const resolveRepositoryId = async (api, projectId, reference) => {
@@ -6447,6 +7395,7 @@ const taskTools = [
6447
7395
  //#region src/mcp/tools/index.ts
6448
7396
  const kcloudTools = [
6449
7397
  ...accountTools,
7398
+ ...organizationTools,
6450
7399
  ...projectTools,
6451
7400
  ...credentialTools,
6452
7401
  ...repositoryTools,
@@ -6457,13 +7406,15 @@ const kcloudTools = [
6457
7406
  ...notificationTools,
6458
7407
  ...releaseTools
6459
7408
  ];
7409
+ const passwordPrinted = (what) => `a secret never passes through the context of a model, and kcloud prints ${what}`;
7410
+ passwordPrinted("the temporary password of the new member"), passwordPrinted("the new temporary password");
6460
7411
  //#endregion
6461
7412
  //#region src/mcp/server.ts
6462
7413
  const instructions = [
6463
- "These tools act on one Kikimora Cloud instance as the account kcloud is signed in with; whoami names both.",
7414
+ "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
7415
  "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
7416
  "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."
7417
+ "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
7418
  ].join("\n");
6468
7419
  const createKcloudServer = ({ version, open }) => {
6469
7420
  const server = new McpServer({
@@ -6484,8 +7435,8 @@ const createKcloudServer = ({ version, open }) => {
6484
7435
  const createMcpCommand = (adaptersOf, cliVersion) => defineCommand({
6485
7436
  name: "mcp",
6486
7437
  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"]],
7438
+ usage: "mcp [--url <origin>] [--org <organization>]",
7439
+ 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
7440
  options: {},
6490
7441
  positionals: {
6491
7442
  min: 0,
@@ -6504,13 +7455,12 @@ const createMcpCommand = (adaptersOf, cliVersion) => defineCommand({
6504
7455
  });
6505
7456
  return {
6506
7457
  api: session.api,
7458
+ me: session.me,
7459
+ organizations: session.organizations,
7460
+ organization: session.organization,
6507
7461
  origin: session.origin,
6508
- profileName: session.profileName,
6509
7462
  kcloudVersion: cliVersion,
6510
- token: () => tokenOf({
6511
- settings,
6512
- context
6513
- }),
7463
+ token: () => Promise.resolve(session.token),
6514
7464
  wait: adapters.wait,
6515
7465
  now: adapters.now
6516
7466
  };
@@ -6527,96 +7477,6 @@ const createMcpCommand = (adaptersOf, cliVersion) => defineCommand({
6527
7477
  }
6528
7478
  });
6529
7479
  //#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
7480
  //#region src/commands/releases.ts
6621
7481
  const createReleasesCommand = (adaptersOf) => defineGroup({
6622
7482
  name: "releases",
@@ -6656,7 +7516,7 @@ const kcloudCommands = () => [
6656
7516
  createLogoutCommand(createAdapters),
6657
7517
  createWhoamiCommand(createAdapters),
6658
7518
  createVersionCommand(createAdapters, cliVersion),
6659
- createProfilesCommand(),
7519
+ createOrgsCommand(createAdapters),
6660
7520
  createTokensCommand(createAdapters),
6661
7521
  createAuthCommand(createAdapters),
6662
7522
  createProjectsCommand(createAdapters),