@kikimoradev/kcloud 0.7.6 → 0.8.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 +7 -1
  2. package/dist/main.js +1405 -450
  3. package/package.json +2 -1
package/dist/main.js CHANGED
@@ -10,6 +10,9 @@ import { z } from "zod";
10
10
  import { spawn } from "node:child_process";
11
11
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
12
12
  import createClient from "openapi-fetch";
13
+ import { finished } from "node:stream/promises";
14
+ import { StdioServerTransport, serveStdio } from "@modelcontextprotocol/server/stdio";
15
+ import { McpServer } from "@modelcontextprotocol/server";
13
16
  //#region ../../packages/cli-kit/src/errors.ts
14
17
  const exitCodes = {
15
18
  ok: 0,
@@ -2442,11 +2445,12 @@ const metricsRangeSchema = z.enum([
2442
2445
  "7d",
2443
2446
  "90d"
2444
2447
  ]);
2445
- const agentMetricsRangeSchema = z.enum([
2448
+ const agentMetricsRanges = [
2446
2449
  "1h",
2447
2450
  "24h",
2448
2451
  "7d"
2449
- ]);
2452
+ ];
2453
+ const agentMetricsRangeSchema = z.enum(agentMetricsRanges);
2450
2454
  const metricsResolutionSchema = z.enum(["minute", "hour"]);
2451
2455
  const instantSchema = z.iso.datetime();
2452
2456
  const byteCountSchema = z.int().nonnegative();
@@ -3316,13 +3320,36 @@ const agentPlanOf = (agent) => ({
3316
3320
  });
3317
3321
  //#endregion
3318
3322
  //#region src/commands/agents/config.ts
3319
- const listedProjects = async (run, project) => project === void 0 ? (await projectsOf(run.api)).map(({ id, slug }) => ({
3320
- id,
3321
- slug
3322
- })) : [{
3323
- id: await resolveProjectId(run.api, project),
3324
- slug: project
3325
- }];
3323
+ const agentsOf = async (api, project) => {
3324
+ const projects = project === void 0 ? (await projectsOf(api)).map(({ id, slug }) => ({
3325
+ id,
3326
+ slug
3327
+ })) : [{
3328
+ id: await resolveProjectId(api, project),
3329
+ slug: project
3330
+ }];
3331
+ const listed = [];
3332
+ for (const { id, slug } of projects) {
3333
+ const { items } = dataOf("list the agents of the project", await api.GET("/projects/{id}/agents", { params: { path: { id } } }), agentHints);
3334
+ for (const agent of items) listed.push({
3335
+ project: slug,
3336
+ ...agent
3337
+ });
3338
+ }
3339
+ return listed;
3340
+ };
3341
+ const changedAgent = async (api, agentId, document) => {
3342
+ const current = dataOf("describe the agent", await api.GET("/agents/{id}", { params: { path: { id: agentId } } }), agentHints);
3343
+ const change = typeof document === "object" && document !== null && "version" in document ? document : {
3344
+ ...document,
3345
+ version: current.version
3346
+ };
3347
+ validated(updateAgentBodySchema, change, "a change to an agent");
3348
+ return dataOf("change the agent", await api.PATCH("/agents/{id}", {
3349
+ params: { path: { id: agentId } },
3350
+ body: change
3351
+ }), agentHints);
3352
+ };
3326
3353
  const configCommands = (adaptersOf) => [
3327
3354
  defineCommand({
3328
3355
  name: "list",
@@ -3336,15 +3363,7 @@ const configCommands = (adaptersOf) => [
3336
3363
  },
3337
3364
  run: async (invocation) => {
3338
3365
  const run = await runOf(adaptersOf, invocation);
3339
- const projects = await listedProjects(run, invocation.values.project);
3340
- const listed = [];
3341
- for (const project of projects) {
3342
- const { items } = dataOf("list the agents of the project", await run.api.GET("/projects/{id}/agents", { params: { path: { id: project.id } } }), agentHints);
3343
- for (const agent of items) listed.push({
3344
- project: project.slug,
3345
- ...agent
3346
- });
3347
- }
3366
+ const listed = await agentsOf(run.api, invocation.values.project);
3348
3367
  return printed(run, listed, (items) => items.length === 0 ? "There is no agent here yet; kcloud agents create makes one." : renderTable([
3349
3368
  { header: "PROJECT" },
3350
3369
  { header: "AGENT" },
@@ -3409,18 +3428,7 @@ const configCommands = (adaptersOf) => [
3409
3428
  const agentId = await agentFrom(run, invocation.positionals[0], invocation.values.project);
3410
3429
  const file = invocation.values.file;
3411
3430
  if (file === void 0) throw new UsageError("agents set needs --file <path>.", "The file holds the sections to change; kcloud agents show <agent> --json shows the current ones.");
3412
- const current = dataOf("describe the agent", await run.api.GET("/agents/{id}", { params: { path: { id: agentId } } }), agentHints);
3413
- const document = await documentOf(run, file);
3414
- const change = typeof document === "object" && document !== null && "version" in document ? document : {
3415
- ...document,
3416
- version: current.version
3417
- };
3418
- validated(updateAgentBodySchema, change, "a change to an agent");
3419
- const body = change;
3420
- const agent = dataOf("change the agent", await run.api.PATCH("/agents/{id}", {
3421
- params: { path: { id: agentId } },
3422
- body
3423
- }), agentHints);
3431
+ const agent = await changedAgent(run.api, agentId, await documentOf(run, file));
3424
3432
  return printed(run, agent, (changed) => changed.restartRequired ? `Changed ${changed.name}; it needs kcloud agents update to take the change.` : `Changed ${changed.name}.`);
3425
3433
  }
3426
3434
  }),
@@ -3778,16 +3786,39 @@ const pageOptions = {
3778
3786
  default: false
3779
3787
  }
3780
3788
  };
3781
- const pageUsage = [["--limit <n>", `how many rows (default: 50)`], ["--all", "follow the cursor to the end instead of one page"]];
3789
+ const allUsage = ["--all", "follow the cursor to the end instead of one page"];
3790
+ const pageUsage = [["--limit <n>", `how many rows (default: 50)`], allUsage];
3782
3791
  const dayOf = (date) => date.toISOString().slice(0, 10);
3792
+ const costWindowOf = (days, now) => ({
3793
+ from: dayOf(/* @__PURE__ */ new Date(now.getTime() - (days - 1) * 24 * 60 * 6e4)),
3794
+ to: dayOf(now)
3795
+ });
3783
3796
  const windowFrom = (days, now) => {
3784
3797
  const span = days === void 0 ? 7 : Number(days);
3785
3798
  if (days !== void 0 && (!/^[0-9]+$/u.test(days) || span < 1 || span > 365)) throw new UsageError(`--days ${days} is not a whole number of days from 1 to 365.`);
3799
+ return costWindowOf(span, now);
3800
+ };
3801
+ const transcriptHints = {
3802
+ ...agentHints,
3803
+ "session.not_found": "kcloud agents sessions <agent> shows the sessions and their ids.",
3804
+ "transcript.unreadable": "The transcript of that session cannot be read."
3805
+ };
3806
+ const transcriptOf = async (api, agentId, sessionId, all) => collect(async (cursor) => {
3807
+ const answer = dataOf("read the transcript", await api.GET("/agents/{id}/sessions/{sessionId}/transcript", { params: {
3808
+ path: {
3809
+ id: agentId,
3810
+ sessionId
3811
+ },
3812
+ ...cursor === null ? {} : { query: { cursor } }
3813
+ } }), transcriptHints);
3786
3814
  return {
3787
- from: dayOf(/* @__PURE__ */ new Date(now.getTime() - (span - 1) * 24 * 60 * 6e4)),
3788
- to: dayOf(now)
3815
+ items: answer.entries,
3816
+ nextCursor: answer.nextCursor
3789
3817
  };
3790
- };
3818
+ }, {
3819
+ limit: 200,
3820
+ all
3821
+ });
3791
3822
  const observabilityCommands = (adaptersOf) => [
3792
3823
  defineCommand({
3793
3824
  name: "sessions",
@@ -3848,10 +3879,10 @@ const observabilityCommands = (adaptersOf) => [
3848
3879
  name: "transcript",
3849
3880
  summary: "Print the transcript of one session of an agent.",
3850
3881
  usage: "agents transcript <agent> <session> [--project <name>] [--all]",
3851
- optionsUsage: [...agentUsage, ...pageUsage],
3882
+ optionsUsage: [...agentUsage, allUsage],
3852
3883
  options: {
3853
3884
  ...agentOption,
3854
- ...pageOptions
3885
+ all: pageOptions.all
3855
3886
  },
3856
3887
  positionals: {
3857
3888
  min: 2,
@@ -3860,27 +3891,7 @@ const observabilityCommands = (adaptersOf) => [
3860
3891
  run: async (invocation) => {
3861
3892
  const run = await runOf(adaptersOf, invocation);
3862
3893
  const agentId = await agentFrom(run, invocation.positionals[0], invocation.values.project);
3863
- const sessionId = invocation.positionals[1] ?? "";
3864
- const page = await collect(async (cursor, limit) => {
3865
- const answer = dataOf("read the transcript", await run.api.GET("/agents/{id}/sessions/{sessionId}/transcript", { params: {
3866
- path: {
3867
- id: agentId,
3868
- sessionId
3869
- },
3870
- query: {
3871
- limit,
3872
- ...cursor === null ? {} : { cursor }
3873
- }
3874
- } }), {
3875
- ...agentHints,
3876
- "session.not_found": "kcloud agents sessions <agent> shows the sessions and their ids.",
3877
- "transcript.unreadable": "The transcript of that session cannot be read."
3878
- });
3879
- return {
3880
- items: answer.entries,
3881
- nextCursor: answer.nextCursor
3882
- };
3883
- }, windowOf(invocation.values.limit, invocation.values.all, 200));
3894
+ const page = await transcriptOf(run.api, agentId, invocation.positionals[1] ?? "", invocation.values.all);
3884
3895
  return printed(run, page.items, (entries) => entries.length === 0 ? "The session left no transcript." : entries.map((entry) => JSON.stringify(entry)).join("\n"));
3885
3896
  }
3886
3897
  }),
@@ -4120,10 +4131,18 @@ const readBody = async (response, request, onId) => {
4120
4131
  reader.cancel();
4121
4132
  }, request.idleMs ?? 1e3);
4122
4133
  };
4134
+ const stop = () => {
4135
+ reader.cancel().catch(() => void 0);
4136
+ };
4137
+ request.signal?.addEventListener("abort", stop, { once: true });
4138
+ if (request.signal?.aborted === true) stop();
4123
4139
  armIdle();
4124
4140
  try {
4125
4141
  for (;;) {
4126
- const chunk = await reader.read();
4142
+ const chunk = await reader.read().catch((error) => {
4143
+ if (request.signal?.aborted === true) return { done: true };
4144
+ throw error;
4145
+ });
4127
4146
  if (chunk.done) break;
4128
4147
  buffer += decoder.decode(chunk.value, { stream: true });
4129
4148
  let boundary = buffer.indexOf("\n\n");
@@ -4147,6 +4166,7 @@ const readBody = async (response, request, onId) => {
4147
4166
  }
4148
4167
  } finally {
4149
4168
  if (timer.current !== null) clearTimeout(timer.current);
4169
+ request.signal?.removeEventListener("abort", stop);
4150
4170
  }
4151
4171
  return {
4152
4172
  closed,
@@ -4166,7 +4186,9 @@ const streamFrames = async (request) => {
4166
4186
  const url = `${request.origin}/api/v1${request.path}${query.size === 0 ? "" : `?${query.toString()}`}`;
4167
4187
  const resume = { lastEventId: null };
4168
4188
  let backoff = 0;
4189
+ const aborted = () => request.signal?.aborted === true;
4169
4190
  for (;;) {
4191
+ if (aborted()) return;
4170
4192
  let response;
4171
4193
  try {
4172
4194
  response = await fetching(url, {
@@ -4175,9 +4197,11 @@ const streamFrames = async (request) => {
4175
4197
  authorization: `Bearer ${request.token}`,
4176
4198
  ...request.resumable === true && resume.lastEventId !== null ? { "last-event-id": resume.lastEventId } : {}
4177
4199
  },
4178
- redirect: "error"
4200
+ redirect: "error",
4201
+ ...request.signal === void 0 ? {} : { signal: request.signal }
4179
4202
  });
4180
4203
  } catch (error) {
4204
+ if (aborted()) return;
4181
4205
  throw unreachable(request.origin, error);
4182
4206
  }
4183
4207
  if (!response.ok) {
@@ -4194,7 +4218,7 @@ const streamFrames = async (request) => {
4194
4218
  resume.lastEventId = id;
4195
4219
  });
4196
4220
  if (closed !== null && fatalReasons.has(closed)) throw new CliError(`The instance closed the stream: ${closed}.`, closed === "session.expired" ? "The token is no longer valid; run kcloud login again." : "You are no longer a member of that project.");
4197
- if (!request.follow) return;
4221
+ if (!request.follow || aborted()) return;
4198
4222
  request.onNotice?.(closed === null ? "The stream ended; opening it again." : `The instance closed the stream: ${closed}. Opening it again.`);
4199
4223
  backoff = nextBackoff(backoff);
4200
4224
  await request.wait(jittered(backoff));
@@ -4288,8 +4312,7 @@ const streamCommands = (adaptersOf) => [{
4288
4312
  },
4289
4313
  onFrame: (frame) => {
4290
4314
  if (frame.event === "agent.state") return;
4291
- if (settings.json) context.terminal.json(frame.data);
4292
- else context.terminal.line(lineOf(frame));
4315
+ context.terminal.line(settings.json ? JSON.stringify(frame.data) : lineOf(frame));
4293
4316
  }
4294
4317
  });
4295
4318
  return exitCodes.ok;
@@ -4505,7 +4528,7 @@ const secretUsage = [
4505
4528
  //#region src/commands/credentials.ts
4506
4529
  const projectOption$2 = { project: { type: "string" } };
4507
4530
  const projectUsage$2 = [["--project <name>", "the project the credential belongs to, by slug, name or id"]];
4508
- const hints$2 = {
4531
+ const credentialHints = {
4509
4532
  "project.not_found": "kcloud projects list shows the projects you are a member of.",
4510
4533
  "credential.not_found": "kcloud credentials list --project <name> shows them.",
4511
4534
  "credential.label_taken": "Another credential of the project already has that label.",
@@ -4515,7 +4538,7 @@ const projectFrom$2 = async (run, given) => {
4515
4538
  if (given === void 0) throw new UsageError("This command needs a project.", "Pass --project <name>; kcloud projects list shows them.");
4516
4539
  return resolveProjectId(run.api, given);
4517
4540
  };
4518
- const credentialsOf = async (api, projectId) => dataOf("list the credentials of the project", await api.GET("/projects/{id}/credentials", { params: { path: { id: projectId } } }), hints$2).items;
4541
+ const credentialsOf = async (api, projectId) => dataOf("list the credentials of the project", await api.GET("/projects/{id}/credentials", { params: { path: { id: projectId } } }), credentialHints).items;
4519
4542
  const resolveCredentialId = async (api, projectId, reference) => {
4520
4543
  const credentials = await credentialsOf(api, projectId);
4521
4544
  const found = credentials.filter((credential) => credential.id === reference || credential.label === reference);
@@ -4601,7 +4624,7 @@ const createCredentialsCommand = (adaptersOf) => defineGroup({
4601
4624
  label,
4602
4625
  secret
4603
4626
  }
4604
- }), hints$2);
4627
+ }), credentialHints);
4605
4628
  return printed(run, credential, (stored) => `Stored ${stored.label} (${stored.fingerprint}) as ${stored.id}.`);
4606
4629
  }
4607
4630
  }),
@@ -4631,7 +4654,7 @@ const createCredentialsCommand = (adaptersOf) => defineGroup({
4631
4654
  credentialId
4632
4655
  } },
4633
4656
  body: { label }
4634
- }), hints$2);
4657
+ }), credentialHints);
4635
4658
  return printed(run, credential, (renamed) => `The credential is ${renamed.label} now.`);
4636
4659
  }
4637
4660
  }),
@@ -4676,7 +4699,7 @@ const createCredentialsCommand = (adaptersOf) => defineGroup({
4676
4699
  secret,
4677
4700
  restartAgents: values["restart-agents"]
4678
4701
  }
4679
- }), hints$2);
4702
+ }), credentialHints);
4680
4703
  return printed(run, credential, (rotated) => `Rotated ${rotated.label} to ${rotated.fingerprint}${values["restart-agents"] ? "; its agents restart" : ""}.`);
4681
4704
  }
4682
4705
  }),
@@ -4697,7 +4720,7 @@ const createCredentialsCommand = (adaptersOf) => defineGroup({
4697
4720
  doneWith("verify the credential", await run.api.POST("/projects/{id}/credentials/{credentialId}/verify", { params: { path: {
4698
4721
  id: projectId,
4699
4722
  credentialId
4700
- } } }), hints$2);
4723
+ } } }), credentialHints);
4701
4724
  return said(run, `The instance is verifying ${credentialId}; kcloud credentials list shows the answer.`);
4702
4725
  }
4703
4726
  }),
@@ -4724,14 +4747,430 @@ const createCredentialsCommand = (adaptersOf) => defineGroup({
4724
4747
  doneWith("delete the credential", await run.api.DELETE("/projects/{id}/credentials/{credentialId}", { params: { path: {
4725
4748
  id: projectId,
4726
4749
  credentialId
4727
- } } }), hints$2);
4750
+ } } }), credentialHints);
4728
4751
  return said(run, `Deleted the credential ${reference}.`);
4729
4752
  }
4730
4753
  })
4731
4754
  ]
4732
4755
  });
4733
4756
  //#endregion
4757
+ //#region src/mcp/tool.ts
4758
+ const defineTool = (definition) => ({
4759
+ ...definition,
4760
+ idempotent: definition.idempotent ?? definition.safety === "read",
4761
+ run: (input, run) => definition.run(definition.input.parse(input), run)
4762
+ });
4763
+ const annotationsOf = (tool) => ({
4764
+ title: tool.title,
4765
+ readOnlyHint: tool.safety === "read",
4766
+ destructiveHint: tool.safety === "destructive",
4767
+ idempotentHint: tool.idempotent,
4768
+ openWorldHint: false
4769
+ });
4770
+ const textOf = (text, isError) => ({
4771
+ content: [{
4772
+ type: "text",
4773
+ text
4774
+ }],
4775
+ ...isError ? { isError } : {}
4776
+ });
4777
+ const apiErrorOf = (error) => {
4778
+ if (error instanceof ApiError) return error;
4779
+ return error instanceof Error ? apiErrorOf(error.cause) : null;
4780
+ };
4781
+ const refusalOf = (error) => {
4782
+ const details = apiErrorOf(error)?.details ?? {};
4783
+ return [
4784
+ error.message,
4785
+ ...error.hint === null ? [] : [error.hint],
4786
+ ...Object.keys(details).length === 0 ? [] : [`Details: ${JSON.stringify(details)}`]
4787
+ ].join("\n");
4788
+ };
4789
+ const resultOf = async (answer) => {
4790
+ try {
4791
+ return textOf(JSON.stringify(await answer()), false);
4792
+ } catch (error) {
4793
+ if (error instanceof z.ZodError) return textOf(z.prettifyError(error), true);
4794
+ if (error instanceof CliError) return textOf(refusalOf(error), true);
4795
+ throw error;
4796
+ }
4797
+ };
4798
+ //#endregion
4799
+ //#region src/commands/tokens.ts
4800
+ const lifetimeOf = (given) => {
4801
+ if (given === void 0) return null;
4802
+ const days = Number(given);
4803
+ if (!/^[0-9]+$/u.test(given) || days < 1 || days > 365) throw new UsageError(`--expires-in ${given} is not a whole number of days from 1 to 365.`);
4804
+ return days;
4805
+ };
4806
+ const tokenHints = { "token.not_found": "kcloud tokens list shows the tokens and their ids." };
4807
+ const createTokensCommand = (adaptersOf) => defineGroup({
4808
+ name: "tokens",
4809
+ summary: "The access tokens of your account on this instance.",
4810
+ usage: "tokens <command>",
4811
+ subcommands: [
4812
+ defineCommand({
4813
+ name: "list",
4814
+ summary: "List the access tokens of your account.",
4815
+ usage: "tokens list",
4816
+ options: {},
4817
+ positionals: {
4818
+ min: 0,
4819
+ max: 0
4820
+ },
4821
+ run: async ({ settings, context }) => {
4822
+ const adapters = adaptersOf(context);
4823
+ const session = await openSession({
4824
+ settings,
4825
+ context,
4826
+ api: adapters.api
4827
+ });
4828
+ const { items } = dataOf("list the access tokens", await session.api.GET("/auth/me/tokens"));
4829
+ if (settings.json) {
4830
+ context.terminal.json(items);
4831
+ return exitCodes.ok;
4832
+ }
4833
+ context.terminal.line(items.length === 0 ? "Your account holds no access token on this instance." : renderTable([
4834
+ { header: "ID" },
4835
+ { header: "NAME" },
4836
+ { header: "LAST USED" },
4837
+ { header: "EXPIRES" }
4838
+ ], items.map((token) => [
4839
+ token.id,
4840
+ token.name,
4841
+ token.lastUsedAt ?? "never",
4842
+ token.expiresAt ?? "never"
4843
+ ])));
4844
+ return exitCodes.ok;
4845
+ }
4846
+ }),
4847
+ defineCommand({
4848
+ name: "create",
4849
+ summary: "Create an access token and print its secret once.",
4850
+ usage: "tokens create <name> [--expires-in <days>]",
4851
+ optionsUsage: [["--expires-in <days>", `how long the token lives, from 1 to 365 days (default: forever)`]],
4852
+ options: { "expires-in": { type: "string" } },
4853
+ positionals: {
4854
+ min: 1,
4855
+ max: 1
4856
+ },
4857
+ run: async ({ values, positionals, settings, context }) => {
4858
+ const adapters = adaptersOf(context);
4859
+ const expiresInDays = lifetimeOf(values["expires-in"]);
4860
+ const session = await openSession({
4861
+ settings,
4862
+ context,
4863
+ api: adapters.api
4864
+ });
4865
+ const created = dataOf("create an access token", await session.api.POST("/auth/me/tokens", { body: {
4866
+ name: positionals[0] ?? "",
4867
+ expiresInDays
4868
+ } }));
4869
+ if (settings.json) {
4870
+ context.terminal.json(created);
4871
+ return exitCodes.ok;
4872
+ }
4873
+ context.terminal.line(created.secret);
4874
+ context.terminal.error(`The token ${created.accessToken.name} is shown only here; the instance keeps its hash.`);
4875
+ return exitCodes.ok;
4876
+ }
4877
+ }),
4878
+ defineCommand({
4879
+ name: "revoke",
4880
+ summary: "Revoke one access token of your account.",
4881
+ usage: "tokens revoke <id>",
4882
+ options: {},
4883
+ positionals: {
4884
+ min: 1,
4885
+ max: 1
4886
+ },
4887
+ run: async ({ positionals, settings, context }) => {
4888
+ const adapters = adaptersOf(context);
4889
+ const id = positionals[0] ?? "";
4890
+ const session = await openSession({
4891
+ settings,
4892
+ context,
4893
+ api: adapters.api
4894
+ });
4895
+ await confirmed({
4896
+ prompt: adapters.prompter(settings),
4897
+ settings,
4898
+ question: `Revoke the token ${id}? Everything using it stops working.`
4899
+ });
4900
+ doneWith(`revoke the token ${id}`, await session.api.DELETE("/auth/me/tokens/{id}", { params: { path: { id } } }), tokenHints);
4901
+ context.terminal.line(`Revoked the token ${id}.`);
4902
+ return exitCodes.ok;
4903
+ }
4904
+ })
4905
+ ]
4906
+ });
4907
+ //#endregion
4908
+ //#region src/mcp/tools/account.ts
4909
+ const accountTools = [
4910
+ defineTool({
4911
+ name: "whoami",
4912
+ title: "Who am I",
4913
+ description: "Say which instance and which account this server acts as, and whether the version of kcloud matches the version of the instance.",
4914
+ operations: ["getMe", "getPlatformVersion"],
4915
+ safety: "read",
4916
+ input: z.object({}),
4917
+ run: async (_, run) => {
4918
+ const me = dataOf("say who the token belongs to", await run.api.GET("/auth/me"));
4919
+ const platform = dataOf("say which version it runs", await run.api.GET("/version"));
4920
+ return {
4921
+ origin: run.origin,
4922
+ profile: run.profileName,
4923
+ user: me,
4924
+ kcloud: run.kcloudVersion,
4925
+ instance: platform.version,
4926
+ matched: platform.version === run.kcloudVersion
4927
+ };
4928
+ }
4929
+ }),
4930
+ defineTool({
4931
+ name: "tokens_list",
4932
+ title: "List access tokens",
4933
+ description: "List the access tokens of your account, without their secrets.",
4934
+ operations: ["listAccessTokens"],
4935
+ safety: "read",
4936
+ input: z.object({}),
4937
+ run: async (_, run) => dataOf("list the access tokens", await run.api.GET("/auth/me/tokens")).items
4938
+ }),
4939
+ defineTool({
4940
+ name: "token_revoke",
4941
+ title: "Revoke an access token",
4942
+ description: "Revoke one access token of your account. Everything that uses it stops working, this server too when it is the token of this server.",
4943
+ operations: ["revokeAccessToken"],
4944
+ safety: "destructive",
4945
+ input: z.object({ id: idSchema.describe("The id of the token, from tokens_list.") }),
4946
+ run: async ({ id }, run) => {
4947
+ doneWith(`revoke the token ${id}`, await run.api.DELETE("/auth/me/tokens/{id}", { params: { path: { id } } }), tokenHints);
4948
+ return { revoked: id };
4949
+ }
4950
+ }),
4951
+ defineTool({
4952
+ name: "locale_set",
4953
+ title: "Set the language of the panel",
4954
+ description: "Set the language the panel and its messages use for your account.",
4955
+ operations: ["changeOwnLocale"],
4956
+ safety: "write",
4957
+ idempotent: true,
4958
+ input: z.object({ locale: localeSchema }),
4959
+ run: async ({ locale }, run) => dataOf("set the language", await run.api.PATCH("/auth/me/locale", { body: { locale } }))
4960
+ })
4961
+ ];
4962
+ //#endregion
4963
+ //#region src/mcp/fields.ts
4964
+ const projectField = z.string().trim().min(1).describe("The project, by its slug, its name or its id.");
4965
+ const agentFields = {
4966
+ agent: z.string().trim().min(1).describe("The agent, as <project>/<agent>, as its slug or name together with project, or by its id."),
4967
+ project: z.string().trim().min(1).optional().describe("The project of the agent, when agent names the agent alone.")
4968
+ };
4969
+ const pageFields = {
4970
+ limit: z.int().min(1).max(200).optional().describe(`How many items one page holds, up to ${String(200)}.`),
4971
+ all: z.boolean().default(false).describe("Follow the cursor to the end instead of returning one page.")
4972
+ };
4973
+ const windowOfPage = ({ limit, all }, fallback = 50) => ({
4974
+ limit: limit ?? fallback,
4975
+ all
4976
+ });
4977
+ const projectIdOf = (run, project) => resolveProjectId(run.api, project);
4978
+ const agentIdOf = (run, { agent, project }) => resolveAgentId(run.api, agent, project);
4979
+ //#endregion
4980
+ //#region src/mcp/tools/agents.ts
4981
+ const actionOn = async (run, agentId, action, force) => dataOf(`${action} the agent`, await run.api.POST("/agents/{id}/actions/{action}", {
4982
+ params: { path: {
4983
+ id: agentId,
4984
+ action
4985
+ } },
4986
+ body: force ? { force: true } : {}
4987
+ }), agentHints);
4988
+ const agentTools = [
4989
+ defineTool({
4990
+ name: "agents_list",
4991
+ title: "List agents",
4992
+ description: "List the agents of one project, or of every project you can see.",
4993
+ operations: ["listAgents"],
4994
+ safety: "read",
4995
+ input: z.object({ project: projectField.optional().describe("One project instead of all of them.") }),
4996
+ run: async ({ project }, run) => agentsOf(run.api, project)
4997
+ }),
4998
+ defineTool({
4999
+ name: "agent_show",
5000
+ title: "Show an agent",
5001
+ description: "Show one agent with its state, runtime, repositories and the actions it takes now. asPlan returns what agent_create takes instead, to copy an agent.",
5002
+ operations: ["getAgent"],
5003
+ safety: "read",
5004
+ input: z.object({
5005
+ ...agentFields,
5006
+ asPlan: z.boolean().default(false)
5007
+ }),
5008
+ run: async ({ asPlan, ...reference }, run) => {
5009
+ const agent = dataOf("describe the agent", await run.api.GET("/agents/{id}", { params: { path: { id: await agentIdOf(run, reference) } } }), agentHints);
5010
+ return asPlan ? agentPlanOf(agent) : agent;
5011
+ }
5012
+ }),
5013
+ defineTool({
5014
+ name: "agent_create",
5015
+ title: "Create an agent",
5016
+ description: "Create an agent in a project. The credentials, repositories and tools are named by the ids that credentials_list, repositories_list and tools_list return; agent_show with asPlan shows a complete example. The agent stays stopped until agent_action starts it.",
5017
+ operations: ["createAgent"],
5018
+ safety: "write",
5019
+ input: z.object({
5020
+ project: projectField,
5021
+ agent: createAgentBodySchema
5022
+ }),
5023
+ run: async ({ project, agent }, run) => dataOf("create the agent", await run.api.POST("/projects/{id}/agents", {
5024
+ params: { path: { id: await projectIdOf(run, project) } },
5025
+ body: agent
5026
+ }), agentHints)
5027
+ }),
5028
+ defineTool({
5029
+ name: "agent_update",
5030
+ title: "Change an agent",
5031
+ description: "Change sections of an agent. Without version the change applies to the version read just before it. When restartRequired comes back true, agent_action update applies the change.",
5032
+ operations: ["updateAgent"],
5033
+ safety: "write",
5034
+ input: z.object({
5035
+ ...agentFields,
5036
+ version: z.int().nonnegative().optional().describe("The version the change is based on, to refuse a change made meanwhile."),
5037
+ sections: updateAgentSectionsSchema
5038
+ }),
5039
+ run: async ({ version, sections, ...reference }, run) => changedAgent(run.api, await agentIdOf(run, reference), {
5040
+ sections,
5041
+ ...version === void 0 ? {} : { version }
5042
+ })
5043
+ }),
5044
+ defineTool({
5045
+ name: "agent_delete",
5046
+ title: "Delete an agent",
5047
+ description: "Delete an agent with its container, its workspace and its history, after its current session, or at once with now.",
5048
+ operations: ["deleteAgent"],
5049
+ safety: "destructive",
5050
+ input: z.object({
5051
+ ...agentFields,
5052
+ now: z.boolean().default(false)
5053
+ }),
5054
+ run: async ({ now, ...reference }, run) => {
5055
+ const agentId = await agentIdOf(run, reference);
5056
+ doneWith("delete the agent", await run.api.DELETE("/agents/{id}", { params: {
5057
+ path: { id: agentId },
5058
+ ...now ? { query: { force: "true" } } : {}
5059
+ } }), agentHints);
5060
+ return {
5061
+ deleted: agentId,
5062
+ now
5063
+ };
5064
+ }
5065
+ }),
5066
+ defineTool({
5067
+ name: "agent_action",
5068
+ title: "Start, pause, resume, restart or update an agent",
5069
+ description: "start lets the agent work, pause holds it between sessions, resume lets a paused agent work again, restart restarts its container, update replaces the container to apply pending changes. The agent returns with the actions it takes next.",
5070
+ operations: ["runAgentAction"],
5071
+ safety: "write",
5072
+ input: z.object({
5073
+ ...agentFields,
5074
+ action: z.enum([
5075
+ "start",
5076
+ "pause",
5077
+ "resume",
5078
+ "restart",
5079
+ "update"
5080
+ ])
5081
+ }),
5082
+ run: async ({ action, ...reference }, run) => actionOn(run, await agentIdOf(run, reference), action, false)
5083
+ }),
5084
+ defineTool({
5085
+ name: "agent_interrupt",
5086
+ title: "Stop, reset or cancel an agent",
5087
+ description: "stop stops the agent after its current session, or at once with now; reset throws its workspace away and provisions it again; cancel cancels what it is doing.",
5088
+ operations: ["runAgentAction"],
5089
+ safety: "destructive",
5090
+ input: z.object({
5091
+ ...agentFields,
5092
+ action: z.enum([
5093
+ "stop",
5094
+ "reset",
5095
+ "cancel"
5096
+ ]),
5097
+ now: z.boolean().default(false).describe("For stop: stop the container at once.")
5098
+ }),
5099
+ run: async ({ action, now, ...reference }, run) => actionOn(run, await agentIdOf(run, reference), action, action === "stop" && now)
5100
+ })
5101
+ ];
5102
+ //#endregion
5103
+ //#region src/mcp/tools/credentials.ts
5104
+ const credentialField$1 = z.string().trim().min(1).describe("The credential, by its label or its id.");
5105
+ const credentialTools = [
5106
+ defineTool({
5107
+ name: "credentials_list",
5108
+ title: "List credentials",
5109
+ description: "List the Claude and GitHub credentials of a project, with their fingerprints and verification, never their secrets.",
5110
+ operations: ["listCredentials"],
5111
+ safety: "read",
5112
+ input: z.object({ project: projectField }),
5113
+ run: async ({ project }, run) => credentialsOf(run.api, await projectIdOf(run, project))
5114
+ }),
5115
+ defineTool({
5116
+ name: "credential_update",
5117
+ title: "Rename or verify a credential",
5118
+ description: "rename gives a credential a new label; verify asks the instance to check it against its service, and credentials_list shows the answer.",
5119
+ operations: ["renameCredential", "verifyCredential"],
5120
+ safety: "write",
5121
+ input: z.object({
5122
+ project: projectField,
5123
+ credential: credentialField$1,
5124
+ action: z.enum(["rename", "verify"]),
5125
+ label: z.string().trim().min(1).optional().describe("The new label, for rename.")
5126
+ }),
5127
+ run: async ({ project, credential, action, label }, run) => {
5128
+ const projectId = await projectIdOf(run, project);
5129
+ const credentialId = await resolveCredentialId(run.api, projectId, credential);
5130
+ if (action === "verify") {
5131
+ doneWith("verify the credential", await run.api.POST("/projects/{id}/credentials/{credentialId}/verify", { params: { path: {
5132
+ id: projectId,
5133
+ credentialId
5134
+ } } }), credentialHints);
5135
+ return {
5136
+ id: credentialId,
5137
+ verifying: true
5138
+ };
5139
+ }
5140
+ if (label === void 0) throw new UsageError("rename needs a label.");
5141
+ return dataOf("rename the credential", await run.api.PATCH("/projects/{id}/credentials/{credentialId}", {
5142
+ params: { path: {
5143
+ id: projectId,
5144
+ credentialId
5145
+ } },
5146
+ body: { label }
5147
+ }), credentialHints);
5148
+ }
5149
+ }),
5150
+ defineTool({
5151
+ name: "credential_delete",
5152
+ title: "Delete a credential",
5153
+ description: "Delete a credential of a project that no agent and no repository uses.",
5154
+ operations: ["deleteCredential"],
5155
+ safety: "destructive",
5156
+ input: z.object({
5157
+ project: projectField,
5158
+ credential: credentialField$1
5159
+ }),
5160
+ run: async ({ project, credential }, run) => {
5161
+ const projectId = await projectIdOf(run, project);
5162
+ const credentialId = await resolveCredentialId(run.api, projectId, credential);
5163
+ doneWith("delete the credential", await run.api.DELETE("/projects/{id}/credentials/{credentialId}", { params: { path: {
5164
+ id: projectId,
5165
+ credentialId
5166
+ } } }), credentialHints);
5167
+ return { deleted: credentialId };
5168
+ }
5169
+ })
5170
+ ];
5171
+ //#endregion
4734
5172
  //#region src/commands/notifications.ts
5173
+ const notificationHints = { "notification.not_found": "kcloud notifications list shows them." };
4735
5174
  const createNotificationsCommand = (adaptersOf) => defineGroup({
4736
5175
  name: "notifications",
4737
5176
  summary: "What the instance wants you to look at.",
@@ -4795,7 +5234,7 @@ const createNotificationsCommand = (adaptersOf) => defineGroup({
4795
5234
  run: async (invocation) => {
4796
5235
  const run = await runOf(adaptersOf, invocation);
4797
5236
  const id = invocation.positionals[0] ?? "";
4798
- const counted = dataOf("mark the notification as read", await run.api.POST("/notifications/{id}/read", { params: { path: { id } } }), { "notification.not_found": "kcloud notifications list shows them." });
5237
+ const counted = dataOf("mark the notification as read", await run.api.POST("/notifications/{id}/read", { params: { path: { id } } }), notificationHints);
4799
5238
  return printed(run, counted, (shown) => `Read it; ${shown.unreadCount} are left unread.`);
4800
5239
  }
4801
5240
  }),
@@ -4846,8 +5285,7 @@ const createNotificationsCommand = (adaptersOf) => defineGroup({
4846
5285
  },
4847
5286
  onFrame: (frame) => {
4848
5287
  if (frame.event !== "notification") return;
4849
- if (settings.json) context.terminal.json(frame.data);
4850
- else context.terminal.line(JSON.stringify(frame.data));
5288
+ context.terminal.line(JSON.stringify(frame.data));
4851
5289
  }
4852
5290
  });
4853
5291
  return exitCodes.ok;
@@ -4855,97 +5293,522 @@ const createNotificationsCommand = (adaptersOf) => defineGroup({
4855
5293
  })
4856
5294
  ]
4857
5295
  });
5296
+ const notificationTools = [
5297
+ defineTool({
5298
+ name: "notifications_list",
5299
+ title: "List notifications",
5300
+ description: "List your notifications, newest first.",
5301
+ operations: ["listNotifications"],
5302
+ safety: "read",
5303
+ input: z.object({
5304
+ ...pageFields,
5305
+ unread: z.boolean().default(false)
5306
+ }),
5307
+ run: async ({ unread, limit, all }, run) => collect(async (cursor, size) => dataOf("list the notifications", await run.api.GET("/notifications", { params: { query: {
5308
+ limit: size,
5309
+ ...cursor === null ? {} : { cursor },
5310
+ ...unread ? { unread: "true" } : {}
5311
+ } } })), windowOfPage({
5312
+ limit,
5313
+ all
5314
+ }))
5315
+ }),
5316
+ defineTool({
5317
+ name: "notifications_mark_read",
5318
+ title: "Mark notifications as read",
5319
+ description: "Mark one notification as read with id, or every notification with all.",
5320
+ operations: ["markNotificationRead", "markAllNotificationsRead"],
5321
+ safety: "write",
5322
+ idempotent: true,
5323
+ input: z.object({
5324
+ id: idSchema.optional().describe("The notification, from notifications_list."),
5325
+ all: z.boolean().default(false)
5326
+ }),
5327
+ run: async ({ id, all }, run) => {
5328
+ if (all === (id !== void 0)) throw new UsageError("Pass either id or all.");
5329
+ return id === void 0 ? dataOf("mark every notification as read", await run.api.POST("/notifications/read-all")) : dataOf("mark the notification as read", await run.api.POST("/notifications/{id}/read", { params: { path: { id } } }), notificationHints);
5330
+ }
5331
+ }),
5332
+ defineTool({
5333
+ name: "notifications_wait",
5334
+ title: "Wait for notifications",
5335
+ description: "Wait until the instance raises a notification, or until the seconds run out, and return what it raised meanwhile. notifications_list shows the older ones.",
5336
+ operations: ["streamNotifications"],
5337
+ safety: "read",
5338
+ input: z.object({ seconds: z.int().min(1).max(50).default(30) }),
5339
+ run: async ({ seconds }, run) => {
5340
+ const raised = [];
5341
+ const settled = new AbortController();
5342
+ const settling = { timer: null };
5343
+ await streamFrames({
5344
+ origin: run.origin,
5345
+ token: await run.token(),
5346
+ path: "/notifications/stream",
5347
+ follow: true,
5348
+ action: "wait for notifications",
5349
+ wait: run.wait,
5350
+ signal: AbortSignal.any([AbortSignal.timeout(seconds * 1e3), settled.signal]),
5351
+ onFrame: (frame) => {
5352
+ if (frame.event !== "notification") return;
5353
+ settling.timer ??= setTimeout(() => {
5354
+ settled.abort();
5355
+ }, defaultIdleMs);
5356
+ raised.push(frame.data);
5357
+ }
5358
+ });
5359
+ if (settling.timer !== null) clearTimeout(settling.timer);
5360
+ return raised;
5361
+ }
5362
+ })
5363
+ ];
4858
5364
  //#endregion
4859
- //#region src/commands/profiles.ts
4860
- const rowsOf = (store) => Object.entries(store.profiles).map(([name, profile]) => [
4861
- name === store.current ? `${name} *` : name,
4862
- profile.origin,
4863
- profile.email ?? "unknown"
4864
- ]);
4865
- const namedProfile = (store, name) => {
4866
- if (store.profiles[name] === void 0) {
4867
- const known = Object.keys(store.profiles);
4868
- 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(", ")}.`);
5365
+ //#region src/mcp/tools/observability.ts
5366
+ const snapshotMs = 1e4;
5367
+ const sources = {
5368
+ live: {
5369
+ path: "/agents/{id}/live",
5370
+ resumable: false
5371
+ },
5372
+ stored: {
5373
+ path: "/agents/{id}/events",
5374
+ resumable: true
4869
5375
  }
4870
5376
  };
4871
- const createProfilesCommand = () => defineGroup({
4872
- name: "profiles",
4873
- summary: "The instances this machine is signed in to.",
4874
- usage: "profiles <command>",
4875
- subcommands: [
4876
- defineCommand({
4877
- name: "list",
4878
- summary: "List the saved profiles, marking the current one.",
4879
- usage: "profiles list",
4880
- options: {},
4881
- positionals: {
4882
- min: 0,
4883
- max: 0
4884
- },
4885
- run: async ({ settings, context }) => {
4886
- const store = await readProfileStore(context.homeDirectory);
4887
- if (settings.json) {
4888
- context.terminal.json({
4889
- current: store.current,
4890
- profiles: Object.entries(store.profiles).map(([name, profile]) => ({
4891
- name,
4892
- origin: profile.origin,
4893
- email: profile.email
4894
- }))
4895
- });
4896
- return exitCodes.ok;
4897
- }
4898
- const rows = rowsOf(store);
4899
- context.terminal.line(rows.length === 0 ? "No instance is signed in here; kcloud login --url <origin> signs in." : renderTable([
4900
- { header: "PROFILE" },
4901
- { header: "INSTANCE" },
4902
- { header: "ACCOUNT" }
4903
- ], rows));
4904
- return exitCodes.ok;
5377
+ const observabilityTools = [
5378
+ defineTool({
5379
+ name: "agent_sessions",
5380
+ title: "List the sessions of an agent",
5381
+ description: "List the Claude sessions of an agent, newest first, with their role, model, outcome and cost.",
5382
+ operations: ["listAgentSessions"],
5383
+ safety: "read",
5384
+ input: z.object({
5385
+ ...agentFields,
5386
+ ...pageFields,
5387
+ role: z.enum(agentRoles).optional(),
5388
+ task: z.string().trim().min(1).optional().describe("Only the sessions of this task id.")
5389
+ }),
5390
+ run: async ({ role, task, limit, all, ...reference }, run) => {
5391
+ const agentId = await agentIdOf(run, reference);
5392
+ return collect(async (cursor, size) => dataOf("list the sessions of the agent", await run.api.GET("/agents/{id}/sessions", { params: {
5393
+ path: { id: agentId },
5394
+ query: {
5395
+ limit: size,
5396
+ ...cursor === null ? {} : { cursor },
5397
+ ...role === void 0 ? {} : { role },
5398
+ ...task === void 0 ? {} : { taskId: task }
5399
+ }
5400
+ } }), agentHints), windowOfPage({
5401
+ limit,
5402
+ all
5403
+ }));
5404
+ }
5405
+ }),
5406
+ defineTool({
5407
+ name: "agent_transcript",
5408
+ title: "Read a session transcript",
5409
+ description: "Read the transcript of one session of an agent, one page of entries as the instance cuts it, or every entry with all.",
5410
+ operations: ["getAgentSessionTranscript"],
5411
+ safety: "read",
5412
+ input: z.object({
5413
+ ...agentFields,
5414
+ session: z.string().trim().min(1).describe("The session id, from agent_sessions."),
5415
+ all: pageFields.all
5416
+ }),
5417
+ run: async ({ session, all, ...reference }, run) => transcriptOf(run.api, await agentIdOf(run, reference), session, all)
5418
+ }),
5419
+ defineTool({
5420
+ name: "agent_costs",
5421
+ title: "Show what an agent spent",
5422
+ description: "Show what an agent spent over the last days, by day with totals, or by task with byTask.",
5423
+ operations: ["getAgentCosts", "listAgentTaskCosts"],
5424
+ safety: "read",
5425
+ input: z.object({
5426
+ ...agentFields,
5427
+ ...pageFields,
5428
+ days: z.int().min(1).max(365).default(7),
5429
+ byTask: z.boolean().default(false)
5430
+ }),
5431
+ run: async ({ days, byTask, limit, all, ...reference }, run) => {
5432
+ const agentId = await agentIdOf(run, reference);
5433
+ const { from, to } = costWindowOf(days, run.now());
5434
+ if (!byTask) return dataOf("show what the agent spent", await run.api.GET("/agents/{id}/costs", { params: {
5435
+ path: { id: agentId },
5436
+ query: {
5437
+ from,
5438
+ to
5439
+ }
5440
+ } }), agentHints);
5441
+ return collect(async (cursor, size) => dataOf("list what the tasks cost", await run.api.GET("/agents/{id}/costs/tasks", { params: {
5442
+ path: { id: agentId },
5443
+ query: {
5444
+ from,
5445
+ to,
5446
+ limit: size,
5447
+ ...cursor === null ? {} : { cursor }
5448
+ }
5449
+ } }), agentHints), windowOfPage({
5450
+ limit,
5451
+ all
5452
+ }));
5453
+ }
5454
+ }),
5455
+ defineTool({
5456
+ name: "agent_metrics",
5457
+ title: "Show what the container of an agent used",
5458
+ description: "Show the CPU and memory the container of an agent used over a range.",
5459
+ operations: ["getAgentMetrics"],
5460
+ safety: "read",
5461
+ input: z.object({
5462
+ ...agentFields,
5463
+ range: z.enum(agentMetricsRanges).default("1h")
5464
+ }),
5465
+ run: async ({ range, ...reference }, run) => dataOf("show what the agent used", await run.api.GET("/agents/{id}/metrics", { params: {
5466
+ path: { id: await agentIdOf(run, reference) },
5467
+ query: { range }
5468
+ } }), agentHints)
5469
+ }),
5470
+ defineTool({
5471
+ name: "agent_memory_search",
5472
+ title: "Search the memory of an agent",
5473
+ description: "Search what an agent remembers of the tasks it finished.",
5474
+ operations: ["searchAgentMemory"],
5475
+ safety: "read",
5476
+ input: z.object({
5477
+ ...agentFields,
5478
+ query: z.string().max(200).optional(),
5479
+ limit: z.int().min(1).max(100).default(20)
5480
+ }),
5481
+ run: async ({ query, limit, ...reference }, run) => dataOf("search what the agent remembers", await run.api.GET("/agents/{id}/memory", { params: {
5482
+ path: { id: await agentIdOf(run, reference) },
5483
+ query: {
5484
+ limit,
5485
+ ...query === void 0 ? {} : { q: query }
5486
+ }
5487
+ } }), agentHints)
5488
+ }),
5489
+ defineTool({
5490
+ name: "agent_events",
5491
+ title: "Read the events of an agent",
5492
+ description: "Read what an agent wrote: live is what it is writing now, stored is its stored history. The answer holds the events up to now and lastEventId; pass that as since to read only newer events next time.",
5493
+ operations: ["streamAgentLive", "streamAgentEvents"],
5494
+ safety: "read",
5495
+ input: z.object({
5496
+ ...agentFields,
5497
+ source: z.enum(["live", "stored"]).default("stored"),
5498
+ since: z.string().trim().min(1).max(64).optional().describe("Start after this event id."),
5499
+ limit: z.int().min(1).max(500).optional().describe("How many events to replay.")
5500
+ }),
5501
+ run: async ({ source, since, limit, ...reference }, run) => {
5502
+ const agentId = await agentIdOf(run, reference);
5503
+ const { path, resumable } = sources[source];
5504
+ const events = [];
5505
+ let lastEventId = since ?? null;
5506
+ await streamFrames({
5507
+ origin: run.origin,
5508
+ token: await run.token(),
5509
+ path: path.replace("{id}", encodeURIComponent(agentId)),
5510
+ query: {
5511
+ ...since === void 0 ? {} : { since },
5512
+ ...limit === void 0 ? {} : { limit: String(limit) }
5513
+ },
5514
+ follow: false,
5515
+ resumable,
5516
+ idleMs: defaultIdleMs,
5517
+ action: `read the ${source} events of the agent`,
5518
+ wait: run.wait,
5519
+ signal: AbortSignal.timeout(snapshotMs),
5520
+ onFrame: (frame) => {
5521
+ if (frame.id !== null) lastEventId = frame.id;
5522
+ if (frame.event !== "agent.state") events.push(frame.data);
5523
+ }
5524
+ });
5525
+ return {
5526
+ events,
5527
+ lastEventId
5528
+ };
5529
+ }
5530
+ })
5531
+ ];
5532
+ //#endregion
5533
+ //#region src/commands/tools.ts
5534
+ const projectOption$1 = { project: { type: "string" } };
5535
+ const projectUsage$1 = [["--project <name>", "the project the tool belongs to, by slug, name or id"]];
5536
+ const toolHints = {
5537
+ "project.not_found": "kcloud projects list shows the projects you are a member of.",
5538
+ "tool.not_found": "kcloud tools list --project <name> shows them.",
5539
+ "tool.name_taken": "Another tool of the project has that name.",
5540
+ "tool.slug_taken": "Another tool of the project has that slug.",
5541
+ "tool.in_use": "Take it off its agents first.",
5542
+ "tool.incomplete": "Every secret of the tool needs a value.",
5543
+ "concurrency.conflict": "Someone changed the tool meanwhile; read it again and retry."
5544
+ };
5545
+ const projectFrom$1 = async (run, given) => {
5546
+ if (given === void 0) throw new UsageError("This command needs a project.", "Pass --project <name>; kcloud projects list shows them.");
5547
+ return resolveProjectId(run.api, given);
5548
+ };
5549
+ const toolsOf = async (api, projectId) => dataOf("list the tools of the project", await api.GET("/projects/{id}/tools", { params: { path: { id: projectId } } }), toolHints).items;
5550
+ const resolveTool = async (api, projectId, reference) => {
5551
+ const tools = await toolsOf(api, projectId);
5552
+ const found = tools.filter((tool) => tool.id === reference || tool.slug === reference || tool.name === reference);
5553
+ const [first] = found;
5554
+ 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(", ")}.`);
5555
+ if (found.length > 1) throw new CliError(`${reference} names ${found.length} tools of the project.`, "Name it by its slug or its id instead.");
5556
+ return first;
5557
+ };
5558
+ const changedTool = async (api, projectId, tool, document) => {
5559
+ const change = typeof document === "object" && document !== null && "version" in document ? document : {
5560
+ ...document,
5561
+ version: tool.version
5562
+ };
5563
+ validated(updateToolBodySchema, change, "a change to a tool");
5564
+ return dataOf("change the tool", await api.PATCH("/projects/{id}/tools/{toolId}", {
5565
+ params: { path: {
5566
+ id: projectId,
5567
+ toolId: tool.id
5568
+ } },
5569
+ body: change
5570
+ }), toolHints);
5571
+ };
5572
+ const createToolsCommand = (adaptersOf) => defineGroup({
5573
+ name: "tools",
5574
+ summary: "The HTTP APIs and MCP servers a project offers its agents.",
5575
+ usage: "tools <command>",
5576
+ subcommands: [
5577
+ defineCommand({
5578
+ name: "list",
5579
+ summary: "List the tools of a project.",
5580
+ usage: "tools list --project <name>",
5581
+ optionsUsage: projectUsage$1,
5582
+ options: projectOption$1,
5583
+ positionals: {
5584
+ min: 0,
5585
+ max: 0
5586
+ },
5587
+ run: async (invocation) => {
5588
+ const run = await runOf(adaptersOf, invocation);
5589
+ const projectId = await projectFrom$1(run, invocation.values.project);
5590
+ const items = await toolsOf(run.api, projectId);
5591
+ return printed(run, items, (tools) => tools.length === 0 ? "The project offers no tool yet." : renderTable([
5592
+ { header: "SLUG" },
5593
+ { header: "NAME" },
5594
+ { header: "KIND" },
5595
+ { header: "SECRETS" },
5596
+ {
5597
+ header: "AGENTS",
5598
+ align: "right"
5599
+ },
5600
+ { header: "ID" }
5601
+ ], tools.map((tool) => [
5602
+ tool.slug,
5603
+ tool.name,
5604
+ tool.kind,
5605
+ tool.complete ? "complete" : "incomplete",
5606
+ String(tool.agentsUsing),
5607
+ tool.id
5608
+ ])));
4905
5609
  }
4906
5610
  }),
4907
5611
  defineCommand({
4908
- name: "use",
4909
- summary: "Make one of the saved profiles the current one.",
4910
- usage: "profiles use <name>",
4911
- options: {},
5612
+ name: "show",
5613
+ summary: "Show one tool with its configuration.",
5614
+ usage: "tools show <tool> --project <name>",
5615
+ optionsUsage: projectUsage$1,
5616
+ options: projectOption$1,
4912
5617
  positionals: {
4913
5618
  min: 1,
4914
5619
  max: 1
4915
5620
  },
4916
- run: async ({ positionals, context }) => {
4917
- const [name = ""] = positionals;
4918
- const store = await readProfileStore(context.homeDirectory);
4919
- namedProfile(store, name);
4920
- await writeProfileStore(context.homeDirectory, {
4921
- ...store,
4922
- current: name
4923
- });
4924
- context.terminal.line(`${name} is the current profile.`);
4925
- return exitCodes.ok;
5621
+ run: async (invocation) => {
5622
+ const run = await runOf(adaptersOf, invocation);
5623
+ const projectId = await projectFrom$1(run, invocation.values.project);
5624
+ const tool = await resolveTool(run.api, projectId, invocation.positionals[0] ?? "");
5625
+ return printed(run, tool, (shown) => [
5626
+ renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
5627
+ ["name", shown.name],
5628
+ ["slug", shown.slug],
5629
+ ["kind", shown.kind],
5630
+ ["version", String(shown.version)],
5631
+ ["complete", shown.complete ? "yes" : "no"],
5632
+ ["agents", String(shown.agentsUsing)],
5633
+ ["id", shown.id]
5634
+ ]),
5635
+ "",
5636
+ JSON.stringify(shown.config, null, 2)
5637
+ ].join("\n"));
4926
5638
  }
4927
5639
  }),
4928
5640
  defineCommand({
4929
- name: "remove",
4930
- summary: "Forget a saved profile without revoking its token.",
4931
- usage: "profiles remove <name>",
4932
- options: {},
5641
+ name: "create",
5642
+ summary: "Create a tool from a JSON description of it.",
5643
+ usage: "tools create --project <name> --file <path>",
5644
+ optionsUsage: [...projectUsage$1, ...fileUsage("the tool")],
5645
+ options: {
5646
+ ...projectOption$1,
5647
+ ...fileOptions
5648
+ },
5649
+ positionals: {
5650
+ min: 0,
5651
+ max: 0
5652
+ },
5653
+ run: async (invocation) => {
5654
+ const run = await runOf(adaptersOf, invocation);
5655
+ const projectId = await projectFrom$1(run, invocation.values.project);
5656
+ const file = invocation.values.file;
5657
+ if (file === void 0) throw new UsageError("tools create needs --file <path>.", "kcloud tools show <tool> --json prints the shape of a tool.");
5658
+ const document = await documentOf(run, file);
5659
+ validated(createToolBodySchema, document, "a tool");
5660
+ const body = document;
5661
+ const tool = dataOf("create the tool", await run.api.POST("/projects/{id}/tools", {
5662
+ params: { path: { id: projectId } },
5663
+ body
5664
+ }), toolHints);
5665
+ return printed(run, tool, (created) => `Created ${created.name} (${created.slug}) as ${created.id}.`);
5666
+ }
5667
+ }),
5668
+ defineCommand({
5669
+ name: "update",
5670
+ summary: "Change a tool from a JSON description of the change.",
5671
+ usage: "tools update <tool> --project <name> --file <path>",
5672
+ optionsUsage: [...projectUsage$1, ...fileUsage("the change")],
5673
+ options: {
5674
+ ...projectOption$1,
5675
+ ...fileOptions
5676
+ },
4933
5677
  positionals: {
4934
5678
  min: 1,
4935
5679
  max: 1
4936
5680
  },
4937
- run: async ({ positionals, context }) => {
4938
- const [name = ""] = positionals;
4939
- const store = await readProfileStore(context.homeDirectory);
4940
- namedProfile(store, name);
4941
- await writeProfileStore(context.homeDirectory, withoutProfile(store, name));
4942
- context.terminal.line(`Forgot ${name}; its token stays usable until it is revoked.`);
4943
- return exitCodes.ok;
5681
+ run: async (invocation) => {
5682
+ const run = await runOf(adaptersOf, invocation);
5683
+ const projectId = await projectFrom$1(run, invocation.values.project);
5684
+ const tool = await resolveTool(run.api, projectId, invocation.positionals[0] ?? "");
5685
+ const file = invocation.values.file;
5686
+ if (file === void 0) throw new UsageError("tools update needs --file <path>.", "The file holds the sections to change, and the version it is based on.");
5687
+ const updated = await changedTool(run.api, projectId, tool, await documentOf(run, file));
5688
+ return printed(run, updated, (shown) => `Changed ${shown.name}.`);
5689
+ }
5690
+ }),
5691
+ defineCommand({
5692
+ name: "delete",
5693
+ summary: "Delete a tool no agent uses.",
5694
+ usage: "tools delete <tool> --project <name>",
5695
+ optionsUsage: projectUsage$1,
5696
+ options: projectOption$1,
5697
+ positionals: {
5698
+ min: 1,
5699
+ max: 1
5700
+ },
5701
+ run: async (invocation) => {
5702
+ const run = await runOf(adaptersOf, invocation);
5703
+ const projectId = await projectFrom$1(run, invocation.values.project);
5704
+ const reference = invocation.positionals[0] ?? "";
5705
+ const tool = await resolveTool(run.api, projectId, reference);
5706
+ await confirmed({
5707
+ prompt: run.prompt,
5708
+ settings: run.settings,
5709
+ question: `Delete the tool ${tool.name}?`
5710
+ });
5711
+ doneWith("delete the tool", await run.api.DELETE("/projects/{id}/tools/{toolId}", { params: { path: {
5712
+ id: projectId,
5713
+ toolId: tool.id
5714
+ } } }), toolHints);
5715
+ return said(run, `Deleted the tool ${tool.name}.`);
4944
5716
  }
4945
5717
  })
4946
5718
  ]
4947
5719
  });
4948
5720
  //#endregion
5721
+ //#region src/mcp/tools/project-tools.ts
5722
+ const toolField = z.string().trim().min(1).describe("The tool, by its slug, its name or its id.");
5723
+ const withoutSecrets = "Secrets never pass through this server: a tool made here holds none and stays incomplete until someone sets them in the panel or with kcloud tools update --file.";
5724
+ const projectToolTools = [
5725
+ defineTool({
5726
+ name: "tools_list",
5727
+ title: "List tools",
5728
+ description: "List the HTTP APIs and MCP servers a project can give its agents, or show one of them with tool.",
5729
+ operations: ["listTools"],
5730
+ safety: "read",
5731
+ input: z.object({
5732
+ project: projectField,
5733
+ tool: toolField.optional()
5734
+ }),
5735
+ run: async ({ project, tool }, run) => {
5736
+ const projectId = await projectIdOf(run, project);
5737
+ return tool === void 0 ? toolsOf(run.api, projectId) : resolveTool(run.api, projectId, tool);
5738
+ }
5739
+ }),
5740
+ defineTool({
5741
+ name: "tool_create",
5742
+ title: "Create a tool",
5743
+ description: `Create an HTTP API or an MCP server tool in a project. ${withoutSecrets}`,
5744
+ operations: ["createTool"],
5745
+ safety: "write",
5746
+ input: z.object({
5747
+ project: projectField,
5748
+ tool: z.strictObject({
5749
+ kind: toolKindSchema,
5750
+ name: toolNameSchema,
5751
+ slug: toolSlugSchema.optional(),
5752
+ description: toolDescriptionSchema,
5753
+ config: toolConfigSchema
5754
+ })
5755
+ }),
5756
+ run: async ({ project, tool }, run) => {
5757
+ const projectId = await projectIdOf(run, project);
5758
+ const body = {
5759
+ ...tool,
5760
+ secrets: {}
5761
+ };
5762
+ validated(createToolBodySchema, body, "a tool");
5763
+ return dataOf("create the tool", await run.api.POST("/projects/{id}/tools", {
5764
+ params: { path: { id: projectId } },
5765
+ body
5766
+ }), toolHints);
5767
+ }
5768
+ }),
5769
+ defineTool({
5770
+ name: "tool_update",
5771
+ title: "Change a tool",
5772
+ description: `Change sections of a tool. Without version the change applies to the version read just before it. ${withoutSecrets}`,
5773
+ operations: ["updateTool"],
5774
+ safety: "write",
5775
+ input: z.object({
5776
+ project: projectField,
5777
+ tool: toolField,
5778
+ version: z.int().nonnegative().optional().describe("The version the change is based on, to refuse a change made meanwhile."),
5779
+ sections: updateToolSectionsSchema.omit({ secrets: true }).strict()
5780
+ }),
5781
+ run: async ({ project, tool, version, sections }, run) => {
5782
+ const projectId = await projectIdOf(run, project);
5783
+ const current = await resolveTool(run.api, projectId, tool);
5784
+ return changedTool(run.api, projectId, current, {
5785
+ sections,
5786
+ ...version === void 0 ? {} : { version }
5787
+ });
5788
+ }
5789
+ }),
5790
+ defineTool({
5791
+ name: "tool_delete",
5792
+ title: "Delete a tool",
5793
+ description: "Delete a tool no agent uses.",
5794
+ operations: ["deleteTool"],
5795
+ safety: "destructive",
5796
+ input: z.object({
5797
+ project: projectField,
5798
+ tool: toolField
5799
+ }),
5800
+ run: async ({ project, tool }, run) => {
5801
+ const projectId = await projectIdOf(run, project);
5802
+ const found = await resolveTool(run.api, projectId, tool);
5803
+ doneWith("delete the tool", await run.api.DELETE("/projects/{id}/tools/{toolId}", { params: { path: {
5804
+ id: projectId,
5805
+ toolId: found.id
5806
+ } } }), toolHints);
5807
+ return { deleted: found.id };
5808
+ }
5809
+ })
5810
+ ];
5811
+ //#endregion
4949
5812
  //#region src/commands/projects.ts
4950
5813
  const projectHint = { "project.not_found": "kcloud projects list shows the projects you are a member of." };
4951
5814
  const createProjectsCommand = (adaptersOf) => defineGroup({
@@ -5036,42 +5899,48 @@ const createProjectsCommand = (adaptersOf) => defineGroup({
5036
5899
  ]
5037
5900
  });
5038
5901
  //#endregion
5039
- //#region src/commands/releases.ts
5040
- const createReleasesCommand = (adaptersOf) => defineGroup({
5041
- name: "releases",
5042
- summary: "The Kikimora releases an agent can run.",
5043
- usage: "releases <command>",
5044
- subcommands: [defineCommand({
5045
- name: "list",
5046
- summary: "List the releases you can give an agent.",
5047
- usage: "releases list",
5048
- options: {},
5049
- positionals: {
5050
- min: 0,
5051
- max: 0
5052
- },
5053
- run: async (invocation) => {
5054
- const run = await runOf(adaptersOf, invocation);
5055
- const releases = dataOf("list the releases", await run.api.GET("/releases"));
5056
- return printed(run, releases, (listed) => listed.items.length === 0 ? "The catalogue holds no release an agent can run." : renderTable([
5057
- { header: "VERSION" },
5058
- { header: "CLAUDE CODE" },
5059
- { header: "PLAYWRIGHT MCP" },
5060
- { header: "PUBLISHED" }
5061
- ], listed.items.map((release) => [
5062
- release.version,
5063
- release.claudeCodeVersion ?? "-",
5064
- release.playwrightMcpVersion ?? "-",
5065
- release.imageCreatedAt ?? "-"
5066
- ])));
5067
- }
5068
- })]
5069
- });
5902
+ //#region src/mcp/tools/projects.ts
5903
+ const projectTools = [defineTool({
5904
+ name: "projects_list",
5905
+ title: "List projects",
5906
+ description: "List the projects you are a member of.",
5907
+ operations: ["listProjects"],
5908
+ safety: "read",
5909
+ input: z.object({}),
5910
+ run: async (_, run) => dataOf("list the projects", await run.api.GET("/projects")).items
5911
+ }), defineTool({
5912
+ name: "project_show",
5913
+ title: "Show a project",
5914
+ description: "Show one project together with its members.",
5915
+ operations: ["getProject", "listProjectMembers"],
5916
+ safety: "read",
5917
+ input: z.object({ project: projectField }),
5918
+ run: async ({ project }, run) => {
5919
+ const id = await projectIdOf(run, project);
5920
+ const shown = dataOf("describe the project", await run.api.GET("/projects/{id}", { params: { path: { id } } }), projectHint);
5921
+ const { items } = dataOf("list the members of the project", await run.api.GET("/projects/{id}/members", { params: { path: { id } } }), projectHint);
5922
+ return {
5923
+ ...shown,
5924
+ members: items
5925
+ };
5926
+ }
5927
+ })];
5928
+ //#endregion
5929
+ //#region src/mcp/tools/releases.ts
5930
+ const releaseTools = [defineTool({
5931
+ name: "releases_list",
5932
+ title: "List releases",
5933
+ description: "List the Kikimora releases an agent can run, with the Claude Code and Playwright MCP versions each one ships.",
5934
+ operations: ["listReleases"],
5935
+ safety: "read",
5936
+ input: z.object({}),
5937
+ run: async (_, run) => dataOf("list the releases", await run.api.GET("/releases")).items
5938
+ })];
5070
5939
  //#endregion
5071
5940
  //#region src/commands/repositories.ts
5072
- const projectOption$1 = { project: { type: "string" } };
5073
- const projectUsage$1 = [["--project <name>", "the project the repository belongs to, by slug, name or id"]];
5074
- const hints$1 = {
5941
+ const projectOption = { project: { type: "string" } };
5942
+ const projectUsage = [["--project <name>", "the project the repository belongs to, by slug, name or id"]];
5943
+ const repositoryHints = {
5075
5944
  "project.not_found": "kcloud projects list shows the projects you are a member of.",
5076
5945
  "repository.not_found": "kcloud repositories list --project <name> shows them.",
5077
5946
  "repository.already_registered": "The project already has that repository.",
@@ -5081,11 +5950,11 @@ const hints$1 = {
5081
5950
  "credential.rejected": "GitHub refused that credential; kcloud credentials rotate <credential> --project <name> replaces it.",
5082
5951
  "credential.not_found": "kcloud credentials list --project <name> shows them."
5083
5952
  };
5084
- const projectFrom$1 = async (run, given) => {
5953
+ const projectFrom = async (run, given) => {
5085
5954
  if (given === void 0) throw new UsageError("This command needs a project.", "Pass --project <name>; kcloud projects list shows them.");
5086
5955
  return resolveProjectId(run.api, given);
5087
5956
  };
5088
- const repositoriesOf = async (api, projectId) => dataOf("list the repositories of the project", await api.GET("/projects/{id}/repositories", { params: { path: { id: projectId } } }), hints$1).items;
5957
+ const repositoriesOf = async (api, projectId) => dataOf("list the repositories of the project", await api.GET("/projects/{id}/repositories", { params: { path: { id: projectId } } }), repositoryHints).items;
5089
5958
  const resolveRepositoryId = async (api, projectId, reference) => {
5090
5959
  const repositories = await repositoriesOf(api, projectId);
5091
5960
  const found = repositories.filter((repository) => repository.id === reference || repository.name === reference || `${repository.owner}/${repository.name}` === reference);
@@ -5108,15 +5977,15 @@ const createRepositoriesCommand = (adaptersOf) => defineGroup({
5108
5977
  name: "list",
5109
5978
  summary: "List the repositories registered in a project.",
5110
5979
  usage: "repositories list --project <name>",
5111
- optionsUsage: projectUsage$1,
5112
- options: projectOption$1,
5980
+ optionsUsage: projectUsage,
5981
+ options: projectOption,
5113
5982
  positionals: {
5114
5983
  min: 0,
5115
5984
  max: 0
5116
5985
  },
5117
5986
  run: async (invocation) => {
5118
5987
  const run = await runOf(adaptersOf, invocation);
5119
- const projectId = await projectFrom$1(run, invocation.values.project);
5988
+ const projectId = await projectFrom(run, invocation.values.project);
5120
5989
  const items = await repositoriesOf(run.api, projectId);
5121
5990
  return printed(run, items, (repositories) => repositories.length === 0 ? "The project has no repository yet." : renderTable([
5122
5991
  { header: "REPOSITORY" },
@@ -5139,9 +6008,9 @@ const createRepositoriesCommand = (adaptersOf) => defineGroup({
5139
6008
  name: "available",
5140
6009
  summary: "List the GitHub repositories one credential can see.",
5141
6010
  usage: "repositories available --project <name> --credential <label>",
5142
- optionsUsage: [...projectUsage$1, ["--credential <label>", "the github_pat credential to look through"]],
6011
+ optionsUsage: [...projectUsage, ["--credential <label>", "the github_pat credential to look through"]],
5143
6012
  options: {
5144
- ...projectOption$1,
6013
+ ...projectOption,
5145
6014
  credential: { type: "string" }
5146
6015
  },
5147
6016
  positionals: {
@@ -5150,14 +6019,14 @@ const createRepositoriesCommand = (adaptersOf) => defineGroup({
5150
6019
  },
5151
6020
  run: async (invocation) => {
5152
6021
  const run = await runOf(adaptersOf, invocation);
5153
- const projectId = await projectFrom$1(run, invocation.values.project);
6022
+ const projectId = await projectFrom(run, invocation.values.project);
5154
6023
  const credential = invocation.values.credential;
5155
6024
  if (credential === void 0) throw new UsageError("repositories available needs --credential <label>.", "kcloud credentials list --project <name> shows them.");
5156
6025
  const credentialId = await resolveCredentialId(run.api, projectId, credential);
5157
6026
  const listed = dataOf("list the repositories of the credential", await run.api.GET("/projects/{id}/repositories/github", { params: {
5158
6027
  path: { id: projectId },
5159
6028
  query: { credentialId }
5160
- } }), hints$1);
6029
+ } }), repositoryHints);
5161
6030
  return printed(run, listed, (page) => [page.items.length === 0 ? "The credential sees no repository." : renderTable([
5162
6031
  { header: "REPOSITORY" },
5163
6032
  { header: "BRANCH" },
@@ -5173,9 +6042,9 @@ const createRepositoriesCommand = (adaptersOf) => defineGroup({
5173
6042
  name: "add",
5174
6043
  summary: "Register a GitHub repository in a project.",
5175
6044
  usage: "repositories add <owner/name> --project <name> --credential <label>",
5176
- optionsUsage: [...projectUsage$1, ["--credential <label>", "the github_pat credential that can see it"]],
6045
+ optionsUsage: [...projectUsage, ["--credential <label>", "the github_pat credential that can see it"]],
5177
6046
  options: {
5178
- ...projectOption$1,
6047
+ ...projectOption,
5179
6048
  credential: { type: "string" }
5180
6049
  },
5181
6050
  positionals: {
@@ -5184,7 +6053,7 @@ const createRepositoriesCommand = (adaptersOf) => defineGroup({
5184
6053
  },
5185
6054
  run: async (invocation) => {
5186
6055
  const run = await runOf(adaptersOf, invocation);
5187
- const projectId = await projectFrom$1(run, invocation.values.project);
6056
+ const projectId = await projectFrom(run, invocation.values.project);
5188
6057
  const [owner, name] = ownerAndName(invocation.positionals[0] ?? "");
5189
6058
  const credential = invocation.values.credential;
5190
6059
  if (credential === void 0) throw new UsageError("repositories add needs --credential <label>.", "kcloud credentials list --project <name> shows them.");
@@ -5196,7 +6065,7 @@ const createRepositoriesCommand = (adaptersOf) => defineGroup({
5196
6065
  name,
5197
6066
  credentialId
5198
6067
  }
5199
- }), hints$1);
6068
+ }), repositoryHints);
5200
6069
  return printed(run, repository, (added) => `Registered ${added.owner}/${added.name} on ${added.defaultBranch} as ${added.id}.`);
5201
6070
  }
5202
6071
  }),
@@ -5204,9 +6073,9 @@ const createRepositoriesCommand = (adaptersOf) => defineGroup({
5204
6073
  name: "set-credential",
5205
6074
  summary: "Point a repository at another credential.",
5206
6075
  usage: "repositories set-credential <owner/name> --project <name> --credential <label>",
5207
- optionsUsage: [...projectUsage$1, ["--credential <label>", "the github_pat credential to use from now on"]],
6076
+ optionsUsage: [...projectUsage, ["--credential <label>", "the github_pat credential to use from now on"]],
5208
6077
  options: {
5209
- ...projectOption$1,
6078
+ ...projectOption,
5210
6079
  credential: { type: "string" }
5211
6080
  },
5212
6081
  positionals: {
@@ -5215,7 +6084,7 @@ const createRepositoriesCommand = (adaptersOf) => defineGroup({
5215
6084
  },
5216
6085
  run: async (invocation) => {
5217
6086
  const run = await runOf(adaptersOf, invocation);
5218
- const projectId = await projectFrom$1(run, invocation.values.project);
6087
+ const projectId = await projectFrom(run, invocation.values.project);
5219
6088
  const repositoryId = await resolveRepositoryId(run.api, projectId, invocation.positionals[0] ?? "");
5220
6089
  const credential = invocation.values.credential;
5221
6090
  if (credential === void 0) throw new UsageError("repositories set-credential needs --credential <label>.");
@@ -5226,7 +6095,7 @@ const createRepositoriesCommand = (adaptersOf) => defineGroup({
5226
6095
  repositoryId
5227
6096
  } },
5228
6097
  body: { credentialId }
5229
- }), hints$1);
6098
+ }), repositoryHints);
5230
6099
  return printed(run, repository, (changed) => `${changed.owner}/${changed.name} uses ${credential} now.`);
5231
6100
  }
5232
6101
  }),
@@ -5234,20 +6103,20 @@ const createRepositoriesCommand = (adaptersOf) => defineGroup({
5234
6103
  name: "refresh",
5235
6104
  summary: "Check a repository against GitHub again.",
5236
6105
  usage: "repositories refresh <owner/name> --project <name>",
5237
- optionsUsage: projectUsage$1,
5238
- options: projectOption$1,
6106
+ optionsUsage: projectUsage,
6107
+ options: projectOption,
5239
6108
  positionals: {
5240
6109
  min: 1,
5241
6110
  max: 1
5242
6111
  },
5243
6112
  run: async (invocation) => {
5244
6113
  const run = await runOf(adaptersOf, invocation);
5245
- const projectId = await projectFrom$1(run, invocation.values.project);
6114
+ const projectId = await projectFrom(run, invocation.values.project);
5246
6115
  const repositoryId = await resolveRepositoryId(run.api, projectId, invocation.positionals[0] ?? "");
5247
6116
  const repository = dataOf("check the repository", await run.api.POST("/projects/{id}/repositories/{repositoryId}/refresh", { params: { path: {
5248
6117
  id: projectId,
5249
6118
  repositoryId
5250
- } } }), hints$1);
6119
+ } } }), repositoryHints);
5251
6120
  return printed(run, repository, (checked) => `${checked.owner}/${checked.name} is ${checked.status} on ${checked.defaultBranch}.`);
5252
6121
  }
5253
6122
  }),
@@ -5255,15 +6124,15 @@ const createRepositoriesCommand = (adaptersOf) => defineGroup({
5255
6124
  name: "delete",
5256
6125
  summary: "Take a repository out of a project.",
5257
6126
  usage: "repositories delete <owner/name> --project <name>",
5258
- optionsUsage: projectUsage$1,
5259
- options: projectOption$1,
6127
+ optionsUsage: projectUsage,
6128
+ options: projectOption,
5260
6129
  positionals: {
5261
6130
  min: 1,
5262
6131
  max: 1
5263
6132
  },
5264
6133
  run: async (invocation) => {
5265
6134
  const run = await runOf(adaptersOf, invocation);
5266
- const projectId = await projectFrom$1(run, invocation.values.project);
6135
+ const projectId = await projectFrom(run, invocation.values.project);
5267
6136
  const reference = invocation.positionals[0] ?? "";
5268
6137
  const repositoryId = await resolveRepositoryId(run.api, projectId, reference);
5269
6138
  await confirmed({
@@ -5274,307 +6143,392 @@ const createRepositoriesCommand = (adaptersOf) => defineGroup({
5274
6143
  doneWith("delete the repository", await run.api.DELETE("/projects/{id}/repositories/{repositoryId}", { params: { path: {
5275
6144
  id: projectId,
5276
6145
  repositoryId
5277
- } } }), hints$1);
6146
+ } } }), repositoryHints);
5278
6147
  return said(run, `Took ${reference} out of the project.`);
5279
6148
  }
5280
6149
  })
5281
6150
  ]
5282
6151
  });
5283
6152
  //#endregion
5284
- //#region src/commands/tools.ts
5285
- const projectOption = { project: { type: "string" } };
5286
- const projectUsage = [["--project <name>", "the project the tool belongs to, by slug, name or id"]];
5287
- const hints = {
5288
- "project.not_found": "kcloud projects list shows the projects you are a member of.",
5289
- "tool.not_found": "kcloud tools list --project <name> shows them.",
5290
- "tool.name_taken": "Another tool of the project has that name.",
5291
- "tool.slug_taken": "Another tool of the project has that slug.",
5292
- "tool.in_use": "Take it off its agents first.",
5293
- "tool.incomplete": "Every secret of the tool needs a value.",
5294
- "concurrency.conflict": "Someone changed the tool meanwhile; read it again and retry."
5295
- };
5296
- const projectFrom = async (run, given) => {
5297
- if (given === void 0) throw new UsageError("This command needs a project.", "Pass --project <name>; kcloud projects list shows them.");
5298
- return resolveProjectId(run.api, given);
5299
- };
5300
- const toolsOf = async (api, projectId) => dataOf("list the tools of the project", await api.GET("/projects/{id}/tools", { params: { path: { id: projectId } } }), hints).items;
5301
- const resolveTool = async (api, projectId, reference) => {
5302
- const tools = await toolsOf(api, projectId);
5303
- const found = tools.filter((tool) => tool.id === reference || tool.slug === reference || tool.name === reference);
5304
- const [first] = found;
5305
- 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(", ")}.`);
5306
- if (found.length > 1) throw new CliError(`${reference} names ${found.length} tools of the project.`, "Name it by its slug or its id instead.");
5307
- return first;
6153
+ //#region src/mcp/tools/repositories.ts
6154
+ const repositoryField = z.string().trim().min(1).describe("The repository, as owner/name, by its name or by its id.");
6155
+ const credentialField = z.string().trim().min(1).describe("A github_pat credential of the project, by its label or its id.");
6156
+ const repositoryTools = [
6157
+ defineTool({
6158
+ name: "repositories_list",
6159
+ title: "List repositories",
6160
+ description: "registered lists the GitHub repositories of a project; github lists what one github_pat credential can see on GitHub, to pick one for repository_add.",
6161
+ operations: ["listRepositories", "listGitHubRepositories"],
6162
+ safety: "read",
6163
+ input: z.object({
6164
+ project: projectField,
6165
+ source: z.enum(["registered", "github"]).default("registered"),
6166
+ credential: credentialField.optional().describe("The credential to look through, for github.")
6167
+ }),
6168
+ run: async ({ project, source, credential }, run) => {
6169
+ const projectId = await projectIdOf(run, project);
6170
+ if (source === "registered") return repositoriesOf(run.api, projectId);
6171
+ if (credential === void 0) throw new UsageError("github needs a credential.");
6172
+ const credentialId = await resolveCredentialId(run.api, projectId, credential);
6173
+ return dataOf("list the repositories of the credential", await run.api.GET("/projects/{id}/repositories/github", { params: {
6174
+ path: { id: projectId },
6175
+ query: { credentialId }
6176
+ } }), repositoryHints);
6177
+ }
6178
+ }),
6179
+ defineTool({
6180
+ name: "repository_add",
6181
+ title: "Register a repository",
6182
+ description: "Register a GitHub repository in a project, so agents can clone it.",
6183
+ operations: ["registerRepository"],
6184
+ safety: "write",
6185
+ input: z.object({
6186
+ project: projectField,
6187
+ repository: z.string().trim().min(1).describe("The GitHub repository, as owner/name."),
6188
+ credential: credentialField
6189
+ }),
6190
+ run: async ({ project, repository, credential }, run) => {
6191
+ const projectId = await projectIdOf(run, project);
6192
+ const [owner, name] = ownerAndName(repository);
6193
+ const credentialId = await resolveCredentialId(run.api, projectId, credential);
6194
+ return dataOf("register the repository", await run.api.POST("/projects/{id}/repositories", {
6195
+ params: { path: { id: projectId } },
6196
+ body: {
6197
+ owner,
6198
+ name,
6199
+ credentialId
6200
+ }
6201
+ }), repositoryHints);
6202
+ }
6203
+ }),
6204
+ defineTool({
6205
+ name: "repository_update",
6206
+ title: "Change or refresh a repository",
6207
+ description: "set_credential points a repository at another github_pat credential; refresh checks it on GitHub again.",
6208
+ operations: ["updateRepository", "refreshRepository"],
6209
+ safety: "write",
6210
+ input: z.object({
6211
+ project: projectField,
6212
+ repository: repositoryField,
6213
+ action: z.enum(["set_credential", "refresh"]),
6214
+ credential: credentialField.optional().describe("The new credential, for set_credential.")
6215
+ }),
6216
+ run: async ({ project, repository, action, credential }, run) => {
6217
+ const projectId = await projectIdOf(run, project);
6218
+ const repositoryId = await resolveRepositoryId(run.api, projectId, repository);
6219
+ if (action === "refresh") return dataOf("check the repository", await run.api.POST("/projects/{id}/repositories/{repositoryId}/refresh", { params: { path: {
6220
+ id: projectId,
6221
+ repositoryId
6222
+ } } }), repositoryHints);
6223
+ if (credential === void 0) throw new UsageError("set_credential needs a credential.");
6224
+ const credentialId = await resolveCredentialId(run.api, projectId, credential);
6225
+ return dataOf("change the credential of the repository", await run.api.PATCH("/projects/{id}/repositories/{repositoryId}", {
6226
+ params: { path: {
6227
+ id: projectId,
6228
+ repositoryId
6229
+ } },
6230
+ body: { credentialId }
6231
+ }), repositoryHints);
6232
+ }
6233
+ }),
6234
+ defineTool({
6235
+ name: "repository_delete",
6236
+ title: "Remove a repository",
6237
+ description: "Take a repository no agent uses out of a project.",
6238
+ operations: ["deleteRepository"],
6239
+ safety: "destructive",
6240
+ input: z.object({
6241
+ project: projectField,
6242
+ repository: repositoryField
6243
+ }),
6244
+ run: async ({ project, repository }, run) => {
6245
+ const projectId = await projectIdOf(run, project);
6246
+ const repositoryId = await resolveRepositoryId(run.api, projectId, repository);
6247
+ doneWith("delete the repository", await run.api.DELETE("/projects/{id}/repositories/{repositoryId}", { params: { path: {
6248
+ id: projectId,
6249
+ repositoryId
6250
+ } } }), repositoryHints);
6251
+ return { deleted: repositoryId };
6252
+ }
6253
+ })
6254
+ ];
6255
+ //#endregion
6256
+ //#region src/mcp/tools/tasks.ts
6257
+ const taskFields = {
6258
+ ...agentFields,
6259
+ task: z.string().trim().min(1).describe("The id of the task, from agent_tasks_list.")
5308
6260
  };
5309
- const createToolsCommand = (adaptersOf) => defineGroup({
5310
- name: "tools",
5311
- summary: "The HTTP APIs and MCP servers a project offers its agents.",
5312
- usage: "tools <command>",
5313
- subcommands: [
5314
- defineCommand({
5315
- name: "list",
5316
- summary: "List the tools of a project.",
5317
- usage: "tools list --project <name>",
5318
- optionsUsage: projectUsage,
5319
- options: projectOption,
5320
- positionals: {
5321
- min: 0,
5322
- max: 0
5323
- },
5324
- run: async (invocation) => {
5325
- const run = await runOf(adaptersOf, invocation);
5326
- const projectId = await projectFrom(run, invocation.values.project);
5327
- const items = await toolsOf(run.api, projectId);
5328
- return printed(run, items, (tools) => tools.length === 0 ? "The project offers no tool yet." : renderTable([
5329
- { header: "SLUG" },
5330
- { header: "NAME" },
5331
- { header: "KIND" },
5332
- { header: "SECRETS" },
5333
- {
5334
- header: "AGENTS",
5335
- align: "right"
5336
- },
5337
- { header: "ID" }
5338
- ], tools.map((tool) => [
5339
- tool.slug,
5340
- tool.name,
5341
- tool.kind,
5342
- tool.complete ? "complete" : "incomplete",
5343
- String(tool.agentsUsing),
5344
- tool.id
5345
- ])));
5346
- }
6261
+ const taskTools = [
6262
+ defineTool({
6263
+ name: "agent_tasks_list",
6264
+ title: "List the tasks of an agent",
6265
+ description: "List the task queue of an agent, with the status, attempts and cost of each task.",
6266
+ operations: ["listAgentTasks"],
6267
+ safety: "read",
6268
+ input: z.object({
6269
+ ...agentFields,
6270
+ status: z.enum(taskStatuses).optional()
5347
6271
  }),
5348
- defineCommand({
5349
- name: "show",
5350
- summary: "Show one tool with its configuration.",
5351
- usage: "tools show <tool> --project <name>",
5352
- optionsUsage: projectUsage,
5353
- options: projectOption,
5354
- positionals: {
5355
- min: 1,
5356
- max: 1
5357
- },
5358
- run: async (invocation) => {
5359
- const run = await runOf(adaptersOf, invocation);
5360
- const projectId = await projectFrom(run, invocation.values.project);
5361
- const tool = await resolveTool(run.api, projectId, invocation.positionals[0] ?? "");
5362
- return printed(run, tool, (shown) => [
5363
- renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
5364
- ["name", shown.name],
5365
- ["slug", shown.slug],
5366
- ["kind", shown.kind],
5367
- ["version", String(shown.version)],
5368
- ["complete", shown.complete ? "yes" : "no"],
5369
- ["agents", String(shown.agentsUsing)],
5370
- ["id", shown.id]
5371
- ]),
5372
- "",
5373
- JSON.stringify(shown.config, null, 2)
5374
- ].join("\n"));
5375
- }
6272
+ run: async ({ status, ...reference }, run) => dataOf("list the tasks of the agent", await run.api.GET("/agents/{id}/tasks", { params: {
6273
+ path: { id: await agentIdOf(run, reference) },
6274
+ ...status === void 0 ? {} : { query: { status } }
6275
+ } }), taskHints)
6276
+ }),
6277
+ defineTool({
6278
+ name: "agent_task_add",
6279
+ title: "Queue a task",
6280
+ description: "Put a task in the queue of an agent.",
6281
+ operations: ["addAgentTask"],
6282
+ safety: "write",
6283
+ input: z.object({
6284
+ ...agentFields,
6285
+ title: z.string().trim().min(1).optional().describe("A short name for the task."),
6286
+ description: z.string().trim().min(1).describe("What the agent should do.")
5376
6287
  }),
5377
- defineCommand({
5378
- name: "create",
5379
- summary: "Create a tool from a JSON description of it.",
5380
- usage: "tools create --project <name> --file <path>",
5381
- optionsUsage: [...projectUsage, ...fileUsage("the tool")],
5382
- options: {
5383
- ...projectOption,
5384
- ...fileOptions
5385
- },
5386
- positionals: {
5387
- min: 0,
5388
- max: 0
5389
- },
5390
- run: async (invocation) => {
5391
- const run = await runOf(adaptersOf, invocation);
5392
- const projectId = await projectFrom(run, invocation.values.project);
5393
- const file = invocation.values.file;
5394
- if (file === void 0) throw new UsageError("tools create needs --file <path>.", "kcloud tools show <tool> --json prints the shape of a tool.");
5395
- const document = await documentOf(run, file);
5396
- validated(createToolBodySchema, document, "a tool");
5397
- const body = document;
5398
- const tool = dataOf("create the tool", await run.api.POST("/projects/{id}/tools", {
5399
- params: { path: { id: projectId } },
5400
- body
5401
- }), hints);
5402
- return printed(run, tool, (created) => `Created ${created.name} (${created.slug}) as ${created.id}.`);
6288
+ run: async ({ title, description, ...reference }, run) => dataOf("add the task", await run.api.POST("/agents/{id}/tasks", {
6289
+ params: { path: { id: await agentIdOf(run, reference) } },
6290
+ body: {
6291
+ description,
6292
+ ...title === void 0 ? {} : { title }
5403
6293
  }
5404
- }),
5405
- defineCommand({
5406
- name: "update",
5407
- summary: "Change a tool from a JSON description of the change.",
5408
- usage: "tools update <tool> --project <name> --file <path>",
5409
- optionsUsage: [...projectUsage, ...fileUsage("the change")],
5410
- options: {
5411
- ...projectOption,
5412
- ...fileOptions
5413
- },
5414
- positionals: {
5415
- min: 1,
5416
- max: 1
5417
- },
5418
- run: async (invocation) => {
5419
- const run = await runOf(adaptersOf, invocation);
5420
- const projectId = await projectFrom(run, invocation.values.project);
5421
- const tool = await resolveTool(run.api, projectId, invocation.positionals[0] ?? "");
5422
- const file = invocation.values.file;
5423
- if (file === void 0) throw new UsageError("tools update needs --file <path>.", "The file holds the sections to change, and the version it is based on.");
5424
- const document = await documentOf(run, file);
5425
- const change = typeof document === "object" && document !== null && "version" in document ? document : {
5426
- ...document,
5427
- version: tool.version
6294
+ }), taskHints)
6295
+ }),
6296
+ defineTool({
6297
+ name: "agent_task_retry",
6298
+ title: "Retry a task",
6299
+ description: "Queue a failed task of an agent again.",
6300
+ operations: ["retryAgentTask"],
6301
+ safety: "write",
6302
+ input: z.object(taskFields),
6303
+ run: async ({ task, ...reference }, run) => {
6304
+ doneWith("retry the task", await run.api.POST("/agents/{id}/tasks/{taskId}/retry", { params: { path: {
6305
+ id: await agentIdOf(run, reference),
6306
+ taskId: task
6307
+ } } }), taskHints);
6308
+ return { queued: task };
6309
+ }
6310
+ }),
6311
+ defineTool({
6312
+ name: "agent_task_cancel",
6313
+ title: "Cancel a task",
6314
+ description: "Cancel a task of an agent that has not started.",
6315
+ operations: ["cancelAgentTask"],
6316
+ safety: "destructive",
6317
+ input: z.object(taskFields),
6318
+ run: async ({ task, ...reference }, run) => {
6319
+ doneWith("cancel the task", await run.api.POST("/agents/{id}/tasks/{taskId}/cancel", { params: { path: {
6320
+ id: await agentIdOf(run, reference),
6321
+ taskId: task
6322
+ } } }), taskHints);
6323
+ return { cancelled: task };
6324
+ }
6325
+ })
6326
+ ];
6327
+ //#endregion
6328
+ //#region src/mcp/tools/index.ts
6329
+ const kcloudTools = [
6330
+ ...accountTools,
6331
+ ...projectTools,
6332
+ ...credentialTools,
6333
+ ...repositoryTools,
6334
+ ...projectToolTools,
6335
+ ...agentTools,
6336
+ ...taskTools,
6337
+ ...observabilityTools,
6338
+ ...notificationTools,
6339
+ ...releaseTools
6340
+ ];
6341
+ //#endregion
6342
+ //#region src/mcp/server.ts
6343
+ const instructions = [
6344
+ "These tools act on one Kikimora Cloud instance as the account kcloud is signed in with; whoami names both.",
6345
+ "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.",
6346
+ "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.",
6347
+ "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."
6348
+ ].join("\n");
6349
+ const createKcloudServer = ({ version, open }) => {
6350
+ const server = new McpServer({
6351
+ name: "kcloud",
6352
+ title: "Kikimora Cloud",
6353
+ version
6354
+ }, { instructions });
6355
+ for (const tool of kcloudTools) server.registerTool(tool.name, {
6356
+ title: tool.title,
6357
+ description: tool.description,
6358
+ inputSchema: tool.input,
6359
+ annotations: annotationsOf(tool)
6360
+ }, (input) => resultOf(async () => tool.run(input, await open())));
6361
+ return server;
6362
+ };
6363
+ //#endregion
6364
+ //#region src/commands/mcp.ts
6365
+ const createMcpCommand = (adaptersOf, cliVersion) => defineCommand({
6366
+ name: "mcp",
6367
+ summary: "Serve the work of kcloud to an MCP client over stdio.",
6368
+ usage: "mcp [--profile <name>] [--url <origin>]",
6369
+ 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"]],
6370
+ options: {},
6371
+ positionals: {
6372
+ min: 0,
6373
+ max: 0
6374
+ },
6375
+ run: async ({ settings, context }) => {
6376
+ const adapters = adaptersOf(context);
6377
+ const { stdin, stdout } = context.io;
6378
+ const connection = serveStdio(() => createKcloudServer({
6379
+ version: cliVersion,
6380
+ open: async () => {
6381
+ const session = await openSession({
6382
+ settings,
6383
+ context,
6384
+ api: adapters.api
6385
+ });
6386
+ return {
6387
+ api: session.api,
6388
+ origin: session.origin,
6389
+ profileName: session.profileName,
6390
+ kcloudVersion: cliVersion,
6391
+ token: () => tokenOf({
6392
+ settings,
6393
+ context
6394
+ }),
6395
+ wait: adapters.wait,
6396
+ now: adapters.now
5428
6397
  };
5429
- validated(updateToolBodySchema, change, "a change to a tool");
5430
- const body = change;
5431
- const updated = dataOf("change the tool", await run.api.PATCH("/projects/{id}/tools/{toolId}", {
5432
- params: { path: {
5433
- id: projectId,
5434
- toolId: tool.id
5435
- } },
5436
- body
5437
- }), hints);
5438
- return printed(run, updated, (shown) => `Changed ${shown.name}.`);
5439
6398
  }
5440
- }),
5441
- defineCommand({
5442
- name: "delete",
5443
- summary: "Delete a tool no agent uses.",
5444
- usage: "tools delete <tool> --project <name>",
5445
- optionsUsage: projectUsage,
5446
- options: projectOption,
5447
- positionals: {
5448
- min: 1,
5449
- max: 1
5450
- },
5451
- run: async (invocation) => {
5452
- const run = await runOf(adaptersOf, invocation);
5453
- const projectId = await projectFrom(run, invocation.values.project);
5454
- const reference = invocation.positionals[0] ?? "";
5455
- const tool = await resolveTool(run.api, projectId, reference);
5456
- await confirmed({
5457
- prompt: run.prompt,
5458
- settings: run.settings,
5459
- question: `Delete the tool ${tool.name}?`
5460
- });
5461
- doneWith("delete the tool", await run.api.DELETE("/projects/{id}/tools/{toolId}", { params: { path: {
5462
- id: projectId,
5463
- toolId: tool.id
5464
- } } }), hints);
5465
- return said(run, `Deleted the tool ${tool.name}.`);
6399
+ }), {
6400
+ transport: new StdioServerTransport(stdin, stdout),
6401
+ onerror: (error) => {
6402
+ context.terminal.error(context.terminal.errorStyles.dim(error.message));
5466
6403
  }
5467
- })
5468
- ]
6404
+ });
6405
+ await finished(stdin, { writable: false }).catch(() => void 0);
6406
+ await connection.close();
6407
+ return exitCodes.ok;
6408
+ }
5469
6409
  });
5470
6410
  //#endregion
5471
- //#region src/commands/tokens.ts
5472
- const lifetimeOf = (given) => {
5473
- if (given === void 0) return null;
5474
- const days = Number(given);
5475
- if (!/^[0-9]+$/u.test(given) || days < 1 || days > 365) throw new UsageError(`--expires-in ${given} is not a whole number of days from 1 to 365.`);
5476
- return days;
6411
+ //#region src/commands/profiles.ts
6412
+ const rowsOf = (store) => Object.entries(store.profiles).map(([name, profile]) => [
6413
+ name === store.current ? `${name} *` : name,
6414
+ profile.origin,
6415
+ profile.email ?? "unknown"
6416
+ ]);
6417
+ const namedProfile = (store, name) => {
6418
+ if (store.profiles[name] === void 0) {
6419
+ const known = Object.keys(store.profiles);
6420
+ 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(", ")}.`);
6421
+ }
5477
6422
  };
5478
- const createTokensCommand = (adaptersOf) => defineGroup({
5479
- name: "tokens",
5480
- summary: "The access tokens of your account on this instance.",
5481
- usage: "tokens <command>",
6423
+ const createProfilesCommand = () => defineGroup({
6424
+ name: "profiles",
6425
+ summary: "The instances this machine is signed in to.",
6426
+ usage: "profiles <command>",
5482
6427
  subcommands: [
5483
6428
  defineCommand({
5484
6429
  name: "list",
5485
- summary: "List the access tokens of your account.",
5486
- usage: "tokens list",
6430
+ summary: "List the saved profiles, marking the current one.",
6431
+ usage: "profiles list",
5487
6432
  options: {},
5488
6433
  positionals: {
5489
6434
  min: 0,
5490
6435
  max: 0
5491
6436
  },
5492
6437
  run: async ({ settings, context }) => {
5493
- const adapters = adaptersOf(context);
5494
- const session = await openSession({
5495
- settings,
5496
- context,
5497
- api: adapters.api
5498
- });
5499
- const { items } = dataOf("list the access tokens", await session.api.GET("/auth/me/tokens"));
6438
+ const store = await readProfileStore(context.homeDirectory);
5500
6439
  if (settings.json) {
5501
- context.terminal.json(items);
6440
+ context.terminal.json({
6441
+ current: store.current,
6442
+ profiles: Object.entries(store.profiles).map(([name, profile]) => ({
6443
+ name,
6444
+ origin: profile.origin,
6445
+ email: profile.email
6446
+ }))
6447
+ });
5502
6448
  return exitCodes.ok;
5503
6449
  }
5504
- context.terminal.line(items.length === 0 ? "Your account holds no access token on this instance." : renderTable([
5505
- { header: "ID" },
5506
- { header: "NAME" },
5507
- { header: "LAST USED" },
5508
- { header: "EXPIRES" }
5509
- ], items.map((token) => [
5510
- token.id,
5511
- token.name,
5512
- token.lastUsedAt ?? "never",
5513
- token.expiresAt ?? "never"
5514
- ])));
6450
+ const rows = rowsOf(store);
6451
+ context.terminal.line(rows.length === 0 ? "No instance is signed in here; kcloud login --url <origin> signs in." : renderTable([
6452
+ { header: "PROFILE" },
6453
+ { header: "INSTANCE" },
6454
+ { header: "ACCOUNT" }
6455
+ ], rows));
5515
6456
  return exitCodes.ok;
5516
6457
  }
5517
6458
  }),
5518
6459
  defineCommand({
5519
- name: "create",
5520
- summary: "Create an access token and print its secret once.",
5521
- usage: "tokens create <name> [--expires-in <days>]",
5522
- optionsUsage: [["--expires-in <days>", `how long the token lives, from 1 to 365 days (default: forever)`]],
5523
- options: { "expires-in": { type: "string" } },
6460
+ name: "use",
6461
+ summary: "Make one of the saved profiles the current one.",
6462
+ usage: "profiles use <name>",
6463
+ options: {},
5524
6464
  positionals: {
5525
6465
  min: 1,
5526
6466
  max: 1
5527
6467
  },
5528
- run: async ({ values, positionals, settings, context }) => {
5529
- const adapters = adaptersOf(context);
5530
- const expiresInDays = lifetimeOf(values["expires-in"]);
5531
- const session = await openSession({
5532
- settings,
5533
- context,
5534
- api: adapters.api
6468
+ run: async ({ positionals, context }) => {
6469
+ const [name = ""] = positionals;
6470
+ const store = await readProfileStore(context.homeDirectory);
6471
+ namedProfile(store, name);
6472
+ await writeProfileStore(context.homeDirectory, {
6473
+ ...store,
6474
+ current: name
5535
6475
  });
5536
- const created = dataOf("create an access token", await session.api.POST("/auth/me/tokens", { body: {
5537
- name: positionals[0] ?? "",
5538
- expiresInDays
5539
- } }));
5540
- if (settings.json) {
5541
- context.terminal.json(created);
5542
- return exitCodes.ok;
5543
- }
5544
- context.terminal.line(created.secret);
5545
- context.terminal.error(`The token ${created.accessToken.name} is shown only here; the instance keeps its hash.`);
6476
+ context.terminal.line(`${name} is the current profile.`);
5546
6477
  return exitCodes.ok;
5547
6478
  }
5548
6479
  }),
5549
6480
  defineCommand({
5550
- name: "revoke",
5551
- summary: "Revoke one access token of your account.",
5552
- usage: "tokens revoke <id>",
6481
+ name: "remove",
6482
+ summary: "Forget a saved profile without revoking its token.",
6483
+ usage: "profiles remove <name>",
5553
6484
  options: {},
5554
6485
  positionals: {
5555
6486
  min: 1,
5556
6487
  max: 1
5557
6488
  },
5558
- run: async ({ positionals, settings, context }) => {
5559
- const adapters = adaptersOf(context);
5560
- const id = positionals[0] ?? "";
5561
- const session = await openSession({
5562
- settings,
5563
- context,
5564
- api: adapters.api
5565
- });
5566
- await confirmed({
5567
- prompt: adapters.prompter(settings),
5568
- settings,
5569
- question: `Revoke the token ${id}? Everything using it stops working.`
5570
- });
5571
- doneWith(`revoke the token ${id}`, await session.api.DELETE("/auth/me/tokens/{id}", { params: { path: { id } } }), { "token.not_found": "kcloud tokens list shows the tokens and their ids." });
5572
- context.terminal.line(`Revoked the token ${id}.`);
6489
+ run: async ({ positionals, context }) => {
6490
+ const [name = ""] = positionals;
6491
+ const store = await readProfileStore(context.homeDirectory);
6492
+ namedProfile(store, name);
6493
+ await writeProfileStore(context.homeDirectory, withoutProfile(store, name));
6494
+ context.terminal.line(`Forgot ${name}; its token stays usable until it is revoked.`);
5573
6495
  return exitCodes.ok;
5574
6496
  }
5575
6497
  })
5576
6498
  ]
5577
6499
  });
6500
+ //#endregion
6501
+ //#region src/commands/releases.ts
6502
+ const createReleasesCommand = (adaptersOf) => defineGroup({
6503
+ name: "releases",
6504
+ summary: "The Kikimora releases an agent can run.",
6505
+ usage: "releases <command>",
6506
+ subcommands: [defineCommand({
6507
+ name: "list",
6508
+ summary: "List the releases you can give an agent.",
6509
+ usage: "releases list",
6510
+ options: {},
6511
+ positionals: {
6512
+ min: 0,
6513
+ max: 0
6514
+ },
6515
+ run: async (invocation) => {
6516
+ const run = await runOf(adaptersOf, invocation);
6517
+ const releases = dataOf("list the releases", await run.api.GET("/releases"));
6518
+ return printed(run, releases, (listed) => listed.items.length === 0 ? "The catalogue holds no release an agent can run." : renderTable([
6519
+ { header: "VERSION" },
6520
+ { header: "CLAUDE CODE" },
6521
+ { header: "PLAYWRIGHT MCP" },
6522
+ { header: "PUBLISHED" }
6523
+ ], listed.items.map((release) => [
6524
+ release.version,
6525
+ release.claudeCodeVersion ?? "-",
6526
+ release.playwrightMcpVersion ?? "-",
6527
+ release.imageCreatedAt ?? "-"
6528
+ ])));
6529
+ }
6530
+ })]
6531
+ });
5578
6532
  const cliVersion = createRequire(import.meta.url)("../package.json").version;
5579
6533
  //#endregion
5580
6534
  //#region src/commands.ts
@@ -5592,7 +6546,8 @@ const kcloudCommands = () => [
5592
6546
  createToolsCommand(createAdapters),
5593
6547
  createAgentsCommand(createAdapters),
5594
6548
  createNotificationsCommand(createAdapters),
5595
- createReleasesCommand(createAdapters)
6549
+ createReleasesCommand(createAdapters),
6550
+ createMcpCommand(createAdapters, cliVersion)
5596
6551
  ];
5597
6552
  //#endregion
5598
6553
  //#region src/main.ts