@kikimoradev/kcloud 0.7.4 → 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 +1416 -451
  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,
@@ -207,6 +210,16 @@ const createClackPrompter = ({ input, output }) => {
207
210
  }
208
211
  };
209
212
  };
213
+ const unanswerableWithoutTerminal = (message) => new UsageError(`"${message}" needs an answer, and stdin is not a terminal.`, "Run the command in a terminal, or pass --yes to take the default answers.");
214
+ const withoutQuestions = (prompter) => ({
215
+ ...prompter,
216
+ text: ({ message }) => Promise.reject(unanswerableWithoutTerminal(message)),
217
+ password: ({ message }) => Promise.reject(unanswerableWithoutTerminal(message)),
218
+ select: ({ message }) => Promise.reject(unanswerableWithoutTerminal(message)),
219
+ confirm: ({ message }) => Promise.reject(unanswerableWithoutTerminal(message))
220
+ });
221
+ const isTerminal$3 = (stream) => stream.isTTY === true;
222
+ const terminalPrompterOf = (streams) => isTerminal$3(streams.input) ? createClackPrompter(streams) : withoutQuestions(createClackPrompter(streams));
210
223
  const unanswerable = (message) => new UsageError(`"${message}" needs an answer, and --yes asks no questions.`, "Pass the value as a flag or an environment variable, or run without --yes.");
211
224
  const createUnattendedPrompter = (write) => ({
212
225
  intro: write,
@@ -2432,11 +2445,12 @@ const metricsRangeSchema = z.enum([
2432
2445
  "7d",
2433
2446
  "90d"
2434
2447
  ]);
2435
- const agentMetricsRangeSchema = z.enum([
2448
+ const agentMetricsRanges = [
2436
2449
  "1h",
2437
2450
  "24h",
2438
2451
  "7d"
2439
- ]);
2452
+ ];
2453
+ const agentMetricsRangeSchema = z.enum(agentMetricsRanges);
2440
2454
  const metricsResolutionSchema = z.enum(["minute", "hour"]);
2441
2455
  const instantSchema = z.iso.datetime();
2442
2456
  const byteCountSchema = z.int().nonnegative();
@@ -2741,7 +2755,7 @@ const createAdapters = (context) => ({
2741
2755
  prompter: (settings) => {
2742
2756
  const asked = settings.yes ? createUnattendedPrompter((text) => {
2743
2757
  context.terminal.error(text);
2744
- }) : createClackPrompter({
2758
+ }) : terminalPrompterOf({
2745
2759
  input: context.io.stdin,
2746
2760
  output: context.io.stderr
2747
2761
  });
@@ -3306,13 +3320,36 @@ const agentPlanOf = (agent) => ({
3306
3320
  });
3307
3321
  //#endregion
3308
3322
  //#region src/commands/agents/config.ts
3309
- const listedProjects = async (run, project) => project === void 0 ? (await projectsOf(run.api)).map(({ id, slug }) => ({
3310
- id,
3311
- slug
3312
- })) : [{
3313
- id: await resolveProjectId(run.api, project),
3314
- slug: project
3315
- }];
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
+ };
3316
3353
  const configCommands = (adaptersOf) => [
3317
3354
  defineCommand({
3318
3355
  name: "list",
@@ -3326,15 +3363,7 @@ const configCommands = (adaptersOf) => [
3326
3363
  },
3327
3364
  run: async (invocation) => {
3328
3365
  const run = await runOf(adaptersOf, invocation);
3329
- const projects = await listedProjects(run, invocation.values.project);
3330
- const listed = [];
3331
- for (const project of projects) {
3332
- const { items } = dataOf("list the agents of the project", await run.api.GET("/projects/{id}/agents", { params: { path: { id: project.id } } }), agentHints);
3333
- for (const agent of items) listed.push({
3334
- project: project.slug,
3335
- ...agent
3336
- });
3337
- }
3366
+ const listed = await agentsOf(run.api, invocation.values.project);
3338
3367
  return printed(run, listed, (items) => items.length === 0 ? "There is no agent here yet; kcloud agents create makes one." : renderTable([
3339
3368
  { header: "PROJECT" },
3340
3369
  { header: "AGENT" },
@@ -3399,18 +3428,7 @@ const configCommands = (adaptersOf) => [
3399
3428
  const agentId = await agentFrom(run, invocation.positionals[0], invocation.values.project);
3400
3429
  const file = invocation.values.file;
3401
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.");
3402
- const current = dataOf("describe the agent", await run.api.GET("/agents/{id}", { params: { path: { id: agentId } } }), agentHints);
3403
- const document = await documentOf(run, file);
3404
- const change = typeof document === "object" && document !== null && "version" in document ? document : {
3405
- ...document,
3406
- version: current.version
3407
- };
3408
- validated(updateAgentBodySchema, change, "a change to an agent");
3409
- const body = change;
3410
- const agent = dataOf("change the agent", await run.api.PATCH("/agents/{id}", {
3411
- params: { path: { id: agentId } },
3412
- body
3413
- }), agentHints);
3431
+ const agent = await changedAgent(run.api, agentId, await documentOf(run, file));
3414
3432
  return printed(run, agent, (changed) => changed.restartRequired ? `Changed ${changed.name}; it needs kcloud agents update to take the change.` : `Changed ${changed.name}.`);
3415
3433
  }
3416
3434
  }),
@@ -3768,16 +3786,39 @@ const pageOptions = {
3768
3786
  default: false
3769
3787
  }
3770
3788
  };
3771
- 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];
3772
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
+ });
3773
3796
  const windowFrom = (days, now) => {
3774
3797
  const span = days === void 0 ? 7 : Number(days);
3775
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);
3776
3814
  return {
3777
- from: dayOf(/* @__PURE__ */ new Date(now.getTime() - (span - 1) * 24 * 60 * 6e4)),
3778
- to: dayOf(now)
3815
+ items: answer.entries,
3816
+ nextCursor: answer.nextCursor
3779
3817
  };
3780
- };
3818
+ }, {
3819
+ limit: 200,
3820
+ all
3821
+ });
3781
3822
  const observabilityCommands = (adaptersOf) => [
3782
3823
  defineCommand({
3783
3824
  name: "sessions",
@@ -3838,10 +3879,10 @@ const observabilityCommands = (adaptersOf) => [
3838
3879
  name: "transcript",
3839
3880
  summary: "Print the transcript of one session of an agent.",
3840
3881
  usage: "agents transcript <agent> <session> [--project <name>] [--all]",
3841
- optionsUsage: [...agentUsage, ...pageUsage],
3882
+ optionsUsage: [...agentUsage, allUsage],
3842
3883
  options: {
3843
3884
  ...agentOption,
3844
- ...pageOptions
3885
+ all: pageOptions.all
3845
3886
  },
3846
3887
  positionals: {
3847
3888
  min: 2,
@@ -3850,27 +3891,7 @@ const observabilityCommands = (adaptersOf) => [
3850
3891
  run: async (invocation) => {
3851
3892
  const run = await runOf(adaptersOf, invocation);
3852
3893
  const agentId = await agentFrom(run, invocation.positionals[0], invocation.values.project);
3853
- const sessionId = invocation.positionals[1] ?? "";
3854
- const page = await collect(async (cursor, limit) => {
3855
- const answer = dataOf("read the transcript", await run.api.GET("/agents/{id}/sessions/{sessionId}/transcript", { params: {
3856
- path: {
3857
- id: agentId,
3858
- sessionId
3859
- },
3860
- query: {
3861
- limit,
3862
- ...cursor === null ? {} : { cursor }
3863
- }
3864
- } }), {
3865
- ...agentHints,
3866
- "session.not_found": "kcloud agents sessions <agent> shows the sessions and their ids.",
3867
- "transcript.unreadable": "The transcript of that session cannot be read."
3868
- });
3869
- return {
3870
- items: answer.entries,
3871
- nextCursor: answer.nextCursor
3872
- };
3873
- }, windowOf(invocation.values.limit, invocation.values.all, 200));
3894
+ const page = await transcriptOf(run.api, agentId, invocation.positionals[1] ?? "", invocation.values.all);
3874
3895
  return printed(run, page.items, (entries) => entries.length === 0 ? "The session left no transcript." : entries.map((entry) => JSON.stringify(entry)).join("\n"));
3875
3896
  }
3876
3897
  }),
@@ -4110,10 +4131,18 @@ const readBody = async (response, request, onId) => {
4110
4131
  reader.cancel();
4111
4132
  }, request.idleMs ?? 1e3);
4112
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();
4113
4139
  armIdle();
4114
4140
  try {
4115
4141
  for (;;) {
4116
- 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
+ });
4117
4146
  if (chunk.done) break;
4118
4147
  buffer += decoder.decode(chunk.value, { stream: true });
4119
4148
  let boundary = buffer.indexOf("\n\n");
@@ -4137,6 +4166,7 @@ const readBody = async (response, request, onId) => {
4137
4166
  }
4138
4167
  } finally {
4139
4168
  if (timer.current !== null) clearTimeout(timer.current);
4169
+ request.signal?.removeEventListener("abort", stop);
4140
4170
  }
4141
4171
  return {
4142
4172
  closed,
@@ -4156,7 +4186,9 @@ const streamFrames = async (request) => {
4156
4186
  const url = `${request.origin}/api/v1${request.path}${query.size === 0 ? "" : `?${query.toString()}`}`;
4157
4187
  const resume = { lastEventId: null };
4158
4188
  let backoff = 0;
4189
+ const aborted = () => request.signal?.aborted === true;
4159
4190
  for (;;) {
4191
+ if (aborted()) return;
4160
4192
  let response;
4161
4193
  try {
4162
4194
  response = await fetching(url, {
@@ -4165,9 +4197,11 @@ const streamFrames = async (request) => {
4165
4197
  authorization: `Bearer ${request.token}`,
4166
4198
  ...request.resumable === true && resume.lastEventId !== null ? { "last-event-id": resume.lastEventId } : {}
4167
4199
  },
4168
- redirect: "error"
4200
+ redirect: "error",
4201
+ ...request.signal === void 0 ? {} : { signal: request.signal }
4169
4202
  });
4170
4203
  } catch (error) {
4204
+ if (aborted()) return;
4171
4205
  throw unreachable(request.origin, error);
4172
4206
  }
4173
4207
  if (!response.ok) {
@@ -4184,7 +4218,7 @@ const streamFrames = async (request) => {
4184
4218
  resume.lastEventId = id;
4185
4219
  });
4186
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.");
4187
- if (!request.follow) return;
4221
+ if (!request.follow || aborted()) return;
4188
4222
  request.onNotice?.(closed === null ? "The stream ended; opening it again." : `The instance closed the stream: ${closed}. Opening it again.`);
4189
4223
  backoff = nextBackoff(backoff);
4190
4224
  await request.wait(jittered(backoff));
@@ -4278,8 +4312,7 @@ const streamCommands = (adaptersOf) => [{
4278
4312
  },
4279
4313
  onFrame: (frame) => {
4280
4314
  if (frame.event === "agent.state") return;
4281
- if (settings.json) context.terminal.json(frame.data);
4282
- else context.terminal.line(lineOf(frame));
4315
+ context.terminal.line(settings.json ? JSON.stringify(frame.data) : lineOf(frame));
4283
4316
  }
4284
4317
  });
4285
4318
  return exitCodes.ok;
@@ -4495,7 +4528,7 @@ const secretUsage = [
4495
4528
  //#region src/commands/credentials.ts
4496
4529
  const projectOption$2 = { project: { type: "string" } };
4497
4530
  const projectUsage$2 = [["--project <name>", "the project the credential belongs to, by slug, name or id"]];
4498
- const hints$2 = {
4531
+ const credentialHints = {
4499
4532
  "project.not_found": "kcloud projects list shows the projects you are a member of.",
4500
4533
  "credential.not_found": "kcloud credentials list --project <name> shows them.",
4501
4534
  "credential.label_taken": "Another credential of the project already has that label.",
@@ -4505,7 +4538,7 @@ const projectFrom$2 = async (run, given) => {
4505
4538
  if (given === void 0) throw new UsageError("This command needs a project.", "Pass --project <name>; kcloud projects list shows them.");
4506
4539
  return resolveProjectId(run.api, given);
4507
4540
  };
4508
- 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;
4509
4542
  const resolveCredentialId = async (api, projectId, reference) => {
4510
4543
  const credentials = await credentialsOf(api, projectId);
4511
4544
  const found = credentials.filter((credential) => credential.id === reference || credential.label === reference);
@@ -4591,7 +4624,7 @@ const createCredentialsCommand = (adaptersOf) => defineGroup({
4591
4624
  label,
4592
4625
  secret
4593
4626
  }
4594
- }), hints$2);
4627
+ }), credentialHints);
4595
4628
  return printed(run, credential, (stored) => `Stored ${stored.label} (${stored.fingerprint}) as ${stored.id}.`);
4596
4629
  }
4597
4630
  }),
@@ -4621,7 +4654,7 @@ const createCredentialsCommand = (adaptersOf) => defineGroup({
4621
4654
  credentialId
4622
4655
  } },
4623
4656
  body: { label }
4624
- }), hints$2);
4657
+ }), credentialHints);
4625
4658
  return printed(run, credential, (renamed) => `The credential is ${renamed.label} now.`);
4626
4659
  }
4627
4660
  }),
@@ -4666,7 +4699,7 @@ const createCredentialsCommand = (adaptersOf) => defineGroup({
4666
4699
  secret,
4667
4700
  restartAgents: values["restart-agents"]
4668
4701
  }
4669
- }), hints$2);
4702
+ }), credentialHints);
4670
4703
  return printed(run, credential, (rotated) => `Rotated ${rotated.label} to ${rotated.fingerprint}${values["restart-agents"] ? "; its agents restart" : ""}.`);
4671
4704
  }
4672
4705
  }),
@@ -4687,7 +4720,7 @@ const createCredentialsCommand = (adaptersOf) => defineGroup({
4687
4720
  doneWith("verify the credential", await run.api.POST("/projects/{id}/credentials/{credentialId}/verify", { params: { path: {
4688
4721
  id: projectId,
4689
4722
  credentialId
4690
- } } }), hints$2);
4723
+ } } }), credentialHints);
4691
4724
  return said(run, `The instance is verifying ${credentialId}; kcloud credentials list shows the answer.`);
4692
4725
  }
4693
4726
  }),
@@ -4714,14 +4747,430 @@ const createCredentialsCommand = (adaptersOf) => defineGroup({
4714
4747
  doneWith("delete the credential", await run.api.DELETE("/projects/{id}/credentials/{credentialId}", { params: { path: {
4715
4748
  id: projectId,
4716
4749
  credentialId
4717
- } } }), hints$2);
4750
+ } } }), credentialHints);
4718
4751
  return said(run, `Deleted the credential ${reference}.`);
4719
4752
  }
4720
4753
  })
4721
4754
  ]
4722
4755
  });
4723
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
4724
5172
  //#region src/commands/notifications.ts
5173
+ const notificationHints = { "notification.not_found": "kcloud notifications list shows them." };
4725
5174
  const createNotificationsCommand = (adaptersOf) => defineGroup({
4726
5175
  name: "notifications",
4727
5176
  summary: "What the instance wants you to look at.",
@@ -4785,7 +5234,7 @@ const createNotificationsCommand = (adaptersOf) => defineGroup({
4785
5234
  run: async (invocation) => {
4786
5235
  const run = await runOf(adaptersOf, invocation);
4787
5236
  const id = invocation.positionals[0] ?? "";
4788
- 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);
4789
5238
  return printed(run, counted, (shown) => `Read it; ${shown.unreadCount} are left unread.`);
4790
5239
  }
4791
5240
  }),
@@ -4836,8 +5285,7 @@ const createNotificationsCommand = (adaptersOf) => defineGroup({
4836
5285
  },
4837
5286
  onFrame: (frame) => {
4838
5287
  if (frame.event !== "notification") return;
4839
- if (settings.json) context.terminal.json(frame.data);
4840
- else context.terminal.line(JSON.stringify(frame.data));
5288
+ context.terminal.line(JSON.stringify(frame.data));
4841
5289
  }
4842
5290
  });
4843
5291
  return exitCodes.ok;
@@ -4845,97 +5293,522 @@ const createNotificationsCommand = (adaptersOf) => defineGroup({
4845
5293
  })
4846
5294
  ]
4847
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
+ ];
4848
5364
  //#endregion
4849
- //#region src/commands/profiles.ts
4850
- const rowsOf = (store) => Object.entries(store.profiles).map(([name, profile]) => [
4851
- name === store.current ? `${name} *` : name,
4852
- profile.origin,
4853
- profile.email ?? "unknown"
4854
- ]);
4855
- const namedProfile = (store, name) => {
4856
- if (store.profiles[name] === void 0) {
4857
- const known = Object.keys(store.profiles);
4858
- 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
4859
5375
  }
4860
5376
  };
4861
- const createProfilesCommand = () => defineGroup({
4862
- name: "profiles",
4863
- summary: "The instances this machine is signed in to.",
4864
- usage: "profiles <command>",
4865
- subcommands: [
4866
- defineCommand({
4867
- name: "list",
4868
- summary: "List the saved profiles, marking the current one.",
4869
- usage: "profiles list",
4870
- options: {},
4871
- positionals: {
4872
- min: 0,
4873
- max: 0
4874
- },
4875
- run: async ({ settings, context }) => {
4876
- const store = await readProfileStore(context.homeDirectory);
4877
- if (settings.json) {
4878
- context.terminal.json({
4879
- current: store.current,
4880
- profiles: Object.entries(store.profiles).map(([name, profile]) => ({
4881
- name,
4882
- origin: profile.origin,
4883
- email: profile.email
4884
- }))
4885
- });
4886
- return exitCodes.ok;
4887
- }
4888
- const rows = rowsOf(store);
4889
- context.terminal.line(rows.length === 0 ? "No instance is signed in here; kcloud login --url <origin> signs in." : renderTable([
4890
- { header: "PROFILE" },
4891
- { header: "INSTANCE" },
4892
- { header: "ACCOUNT" }
4893
- ], rows));
4894
- 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
+ ])));
4895
5609
  }
4896
5610
  }),
4897
5611
  defineCommand({
4898
- name: "use",
4899
- summary: "Make one of the saved profiles the current one.",
4900
- usage: "profiles use <name>",
4901
- 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,
4902
5617
  positionals: {
4903
5618
  min: 1,
4904
5619
  max: 1
4905
5620
  },
4906
- run: async ({ positionals, context }) => {
4907
- const [name = ""] = positionals;
4908
- const store = await readProfileStore(context.homeDirectory);
4909
- namedProfile(store, name);
4910
- await writeProfileStore(context.homeDirectory, {
4911
- ...store,
4912
- current: name
4913
- });
4914
- context.terminal.line(`${name} is the current profile.`);
4915
- 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"));
4916
5638
  }
4917
5639
  }),
4918
5640
  defineCommand({
4919
- name: "remove",
4920
- summary: "Forget a saved profile without revoking its token.",
4921
- usage: "profiles remove <name>",
4922
- 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
+ },
4923
5677
  positionals: {
4924
5678
  min: 1,
4925
5679
  max: 1
4926
5680
  },
4927
- run: async ({ positionals, context }) => {
4928
- const [name = ""] = positionals;
4929
- const store = await readProfileStore(context.homeDirectory);
4930
- namedProfile(store, name);
4931
- await writeProfileStore(context.homeDirectory, withoutProfile(store, name));
4932
- context.terminal.line(`Forgot ${name}; its token stays usable until it is revoked.`);
4933
- 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}.`);
4934
5716
  }
4935
5717
  })
4936
5718
  ]
4937
5719
  });
4938
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
4939
5812
  //#region src/commands/projects.ts
4940
5813
  const projectHint = { "project.not_found": "kcloud projects list shows the projects you are a member of." };
4941
5814
  const createProjectsCommand = (adaptersOf) => defineGroup({
@@ -5026,42 +5899,48 @@ const createProjectsCommand = (adaptersOf) => defineGroup({
5026
5899
  ]
5027
5900
  });
5028
5901
  //#endregion
5029
- //#region src/commands/releases.ts
5030
- const createReleasesCommand = (adaptersOf) => defineGroup({
5031
- name: "releases",
5032
- summary: "The Kikimora releases an agent can run.",
5033
- usage: "releases <command>",
5034
- subcommands: [defineCommand({
5035
- name: "list",
5036
- summary: "List the releases you can give an agent.",
5037
- usage: "releases list",
5038
- options: {},
5039
- positionals: {
5040
- min: 0,
5041
- max: 0
5042
- },
5043
- run: async (invocation) => {
5044
- const run = await runOf(adaptersOf, invocation);
5045
- const releases = dataOf("list the releases", await run.api.GET("/releases"));
5046
- return printed(run, releases, (listed) => listed.items.length === 0 ? "The catalogue holds no release an agent can run." : renderTable([
5047
- { header: "VERSION" },
5048
- { header: "CLAUDE CODE" },
5049
- { header: "PLAYWRIGHT MCP" },
5050
- { header: "PUBLISHED" }
5051
- ], listed.items.map((release) => [
5052
- release.version,
5053
- release.claudeCodeVersion ?? "-",
5054
- release.playwrightMcpVersion ?? "-",
5055
- release.imageCreatedAt ?? "-"
5056
- ])));
5057
- }
5058
- })]
5059
- });
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
+ })];
5060
5939
  //#endregion
5061
5940
  //#region src/commands/repositories.ts
5062
- const projectOption$1 = { project: { type: "string" } };
5063
- const projectUsage$1 = [["--project <name>", "the project the repository belongs to, by slug, name or id"]];
5064
- 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 = {
5065
5944
  "project.not_found": "kcloud projects list shows the projects you are a member of.",
5066
5945
  "repository.not_found": "kcloud repositories list --project <name> shows them.",
5067
5946
  "repository.already_registered": "The project already has that repository.",
@@ -5071,11 +5950,11 @@ const hints$1 = {
5071
5950
  "credential.rejected": "GitHub refused that credential; kcloud credentials rotate <credential> --project <name> replaces it.",
5072
5951
  "credential.not_found": "kcloud credentials list --project <name> shows them."
5073
5952
  };
5074
- const projectFrom$1 = async (run, given) => {
5953
+ const projectFrom = async (run, given) => {
5075
5954
  if (given === void 0) throw new UsageError("This command needs a project.", "Pass --project <name>; kcloud projects list shows them.");
5076
5955
  return resolveProjectId(run.api, given);
5077
5956
  };
5078
- 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;
5079
5958
  const resolveRepositoryId = async (api, projectId, reference) => {
5080
5959
  const repositories = await repositoriesOf(api, projectId);
5081
5960
  const found = repositories.filter((repository) => repository.id === reference || repository.name === reference || `${repository.owner}/${repository.name}` === reference);
@@ -5098,15 +5977,15 @@ const createRepositoriesCommand = (adaptersOf) => defineGroup({
5098
5977
  name: "list",
5099
5978
  summary: "List the repositories registered in a project.",
5100
5979
  usage: "repositories list --project <name>",
5101
- optionsUsage: projectUsage$1,
5102
- options: projectOption$1,
5980
+ optionsUsage: projectUsage,
5981
+ options: projectOption,
5103
5982
  positionals: {
5104
5983
  min: 0,
5105
5984
  max: 0
5106
5985
  },
5107
5986
  run: async (invocation) => {
5108
5987
  const run = await runOf(adaptersOf, invocation);
5109
- const projectId = await projectFrom$1(run, invocation.values.project);
5988
+ const projectId = await projectFrom(run, invocation.values.project);
5110
5989
  const items = await repositoriesOf(run.api, projectId);
5111
5990
  return printed(run, items, (repositories) => repositories.length === 0 ? "The project has no repository yet." : renderTable([
5112
5991
  { header: "REPOSITORY" },
@@ -5129,9 +6008,9 @@ const createRepositoriesCommand = (adaptersOf) => defineGroup({
5129
6008
  name: "available",
5130
6009
  summary: "List the GitHub repositories one credential can see.",
5131
6010
  usage: "repositories available --project <name> --credential <label>",
5132
- optionsUsage: [...projectUsage$1, ["--credential <label>", "the github_pat credential to look through"]],
6011
+ optionsUsage: [...projectUsage, ["--credential <label>", "the github_pat credential to look through"]],
5133
6012
  options: {
5134
- ...projectOption$1,
6013
+ ...projectOption,
5135
6014
  credential: { type: "string" }
5136
6015
  },
5137
6016
  positionals: {
@@ -5140,14 +6019,14 @@ const createRepositoriesCommand = (adaptersOf) => defineGroup({
5140
6019
  },
5141
6020
  run: async (invocation) => {
5142
6021
  const run = await runOf(adaptersOf, invocation);
5143
- const projectId = await projectFrom$1(run, invocation.values.project);
6022
+ const projectId = await projectFrom(run, invocation.values.project);
5144
6023
  const credential = invocation.values.credential;
5145
6024
  if (credential === void 0) throw new UsageError("repositories available needs --credential <label>.", "kcloud credentials list --project <name> shows them.");
5146
6025
  const credentialId = await resolveCredentialId(run.api, projectId, credential);
5147
6026
  const listed = dataOf("list the repositories of the credential", await run.api.GET("/projects/{id}/repositories/github", { params: {
5148
6027
  path: { id: projectId },
5149
6028
  query: { credentialId }
5150
- } }), hints$1);
6029
+ } }), repositoryHints);
5151
6030
  return printed(run, listed, (page) => [page.items.length === 0 ? "The credential sees no repository." : renderTable([
5152
6031
  { header: "REPOSITORY" },
5153
6032
  { header: "BRANCH" },
@@ -5163,9 +6042,9 @@ const createRepositoriesCommand = (adaptersOf) => defineGroup({
5163
6042
  name: "add",
5164
6043
  summary: "Register a GitHub repository in a project.",
5165
6044
  usage: "repositories add <owner/name> --project <name> --credential <label>",
5166
- 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"]],
5167
6046
  options: {
5168
- ...projectOption$1,
6047
+ ...projectOption,
5169
6048
  credential: { type: "string" }
5170
6049
  },
5171
6050
  positionals: {
@@ -5174,7 +6053,7 @@ const createRepositoriesCommand = (adaptersOf) => defineGroup({
5174
6053
  },
5175
6054
  run: async (invocation) => {
5176
6055
  const run = await runOf(adaptersOf, invocation);
5177
- const projectId = await projectFrom$1(run, invocation.values.project);
6056
+ const projectId = await projectFrom(run, invocation.values.project);
5178
6057
  const [owner, name] = ownerAndName(invocation.positionals[0] ?? "");
5179
6058
  const credential = invocation.values.credential;
5180
6059
  if (credential === void 0) throw new UsageError("repositories add needs --credential <label>.", "kcloud credentials list --project <name> shows them.");
@@ -5186,7 +6065,7 @@ const createRepositoriesCommand = (adaptersOf) => defineGroup({
5186
6065
  name,
5187
6066
  credentialId
5188
6067
  }
5189
- }), hints$1);
6068
+ }), repositoryHints);
5190
6069
  return printed(run, repository, (added) => `Registered ${added.owner}/${added.name} on ${added.defaultBranch} as ${added.id}.`);
5191
6070
  }
5192
6071
  }),
@@ -5194,9 +6073,9 @@ const createRepositoriesCommand = (adaptersOf) => defineGroup({
5194
6073
  name: "set-credential",
5195
6074
  summary: "Point a repository at another credential.",
5196
6075
  usage: "repositories set-credential <owner/name> --project <name> --credential <label>",
5197
- 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"]],
5198
6077
  options: {
5199
- ...projectOption$1,
6078
+ ...projectOption,
5200
6079
  credential: { type: "string" }
5201
6080
  },
5202
6081
  positionals: {
@@ -5205,7 +6084,7 @@ const createRepositoriesCommand = (adaptersOf) => defineGroup({
5205
6084
  },
5206
6085
  run: async (invocation) => {
5207
6086
  const run = await runOf(adaptersOf, invocation);
5208
- const projectId = await projectFrom$1(run, invocation.values.project);
6087
+ const projectId = await projectFrom(run, invocation.values.project);
5209
6088
  const repositoryId = await resolveRepositoryId(run.api, projectId, invocation.positionals[0] ?? "");
5210
6089
  const credential = invocation.values.credential;
5211
6090
  if (credential === void 0) throw new UsageError("repositories set-credential needs --credential <label>.");
@@ -5216,7 +6095,7 @@ const createRepositoriesCommand = (adaptersOf) => defineGroup({
5216
6095
  repositoryId
5217
6096
  } },
5218
6097
  body: { credentialId }
5219
- }), hints$1);
6098
+ }), repositoryHints);
5220
6099
  return printed(run, repository, (changed) => `${changed.owner}/${changed.name} uses ${credential} now.`);
5221
6100
  }
5222
6101
  }),
@@ -5224,20 +6103,20 @@ const createRepositoriesCommand = (adaptersOf) => defineGroup({
5224
6103
  name: "refresh",
5225
6104
  summary: "Check a repository against GitHub again.",
5226
6105
  usage: "repositories refresh <owner/name> --project <name>",
5227
- optionsUsage: projectUsage$1,
5228
- options: projectOption$1,
6106
+ optionsUsage: projectUsage,
6107
+ options: projectOption,
5229
6108
  positionals: {
5230
6109
  min: 1,
5231
6110
  max: 1
5232
6111
  },
5233
6112
  run: async (invocation) => {
5234
6113
  const run = await runOf(adaptersOf, invocation);
5235
- const projectId = await projectFrom$1(run, invocation.values.project);
6114
+ const projectId = await projectFrom(run, invocation.values.project);
5236
6115
  const repositoryId = await resolveRepositoryId(run.api, projectId, invocation.positionals[0] ?? "");
5237
6116
  const repository = dataOf("check the repository", await run.api.POST("/projects/{id}/repositories/{repositoryId}/refresh", { params: { path: {
5238
6117
  id: projectId,
5239
6118
  repositoryId
5240
- } } }), hints$1);
6119
+ } } }), repositoryHints);
5241
6120
  return printed(run, repository, (checked) => `${checked.owner}/${checked.name} is ${checked.status} on ${checked.defaultBranch}.`);
5242
6121
  }
5243
6122
  }),
@@ -5245,15 +6124,15 @@ const createRepositoriesCommand = (adaptersOf) => defineGroup({
5245
6124
  name: "delete",
5246
6125
  summary: "Take a repository out of a project.",
5247
6126
  usage: "repositories delete <owner/name> --project <name>",
5248
- optionsUsage: projectUsage$1,
5249
- options: projectOption$1,
6127
+ optionsUsage: projectUsage,
6128
+ options: projectOption,
5250
6129
  positionals: {
5251
6130
  min: 1,
5252
6131
  max: 1
5253
6132
  },
5254
6133
  run: async (invocation) => {
5255
6134
  const run = await runOf(adaptersOf, invocation);
5256
- const projectId = await projectFrom$1(run, invocation.values.project);
6135
+ const projectId = await projectFrom(run, invocation.values.project);
5257
6136
  const reference = invocation.positionals[0] ?? "";
5258
6137
  const repositoryId = await resolveRepositoryId(run.api, projectId, reference);
5259
6138
  await confirmed({
@@ -5264,307 +6143,392 @@ const createRepositoriesCommand = (adaptersOf) => defineGroup({
5264
6143
  doneWith("delete the repository", await run.api.DELETE("/projects/{id}/repositories/{repositoryId}", { params: { path: {
5265
6144
  id: projectId,
5266
6145
  repositoryId
5267
- } } }), hints$1);
6146
+ } } }), repositoryHints);
5268
6147
  return said(run, `Took ${reference} out of the project.`);
5269
6148
  }
5270
6149
  })
5271
6150
  ]
5272
6151
  });
5273
6152
  //#endregion
5274
- //#region src/commands/tools.ts
5275
- const projectOption = { project: { type: "string" } };
5276
- const projectUsage = [["--project <name>", "the project the tool belongs to, by slug, name or id"]];
5277
- const hints = {
5278
- "project.not_found": "kcloud projects list shows the projects you are a member of.",
5279
- "tool.not_found": "kcloud tools list --project <name> shows them.",
5280
- "tool.name_taken": "Another tool of the project has that name.",
5281
- "tool.slug_taken": "Another tool of the project has that slug.",
5282
- "tool.in_use": "Take it off its agents first.",
5283
- "tool.incomplete": "Every secret of the tool needs a value.",
5284
- "concurrency.conflict": "Someone changed the tool meanwhile; read it again and retry."
5285
- };
5286
- const projectFrom = async (run, given) => {
5287
- if (given === void 0) throw new UsageError("This command needs a project.", "Pass --project <name>; kcloud projects list shows them.");
5288
- return resolveProjectId(run.api, given);
5289
- };
5290
- const toolsOf = async (api, projectId) => dataOf("list the tools of the project", await api.GET("/projects/{id}/tools", { params: { path: { id: projectId } } }), hints).items;
5291
- const resolveTool = async (api, projectId, reference) => {
5292
- const tools = await toolsOf(api, projectId);
5293
- const found = tools.filter((tool) => tool.id === reference || tool.slug === reference || tool.name === reference);
5294
- const [first] = found;
5295
- 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(", ")}.`);
5296
- if (found.length > 1) throw new CliError(`${reference} names ${found.length} tools of the project.`, "Name it by its slug or its id instead.");
5297
- 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.")
5298
6260
  };
5299
- const createToolsCommand = (adaptersOf) => defineGroup({
5300
- name: "tools",
5301
- summary: "The HTTP APIs and MCP servers a project offers its agents.",
5302
- usage: "tools <command>",
5303
- subcommands: [
5304
- defineCommand({
5305
- name: "list",
5306
- summary: "List the tools of a project.",
5307
- usage: "tools list --project <name>",
5308
- optionsUsage: projectUsage,
5309
- options: projectOption,
5310
- positionals: {
5311
- min: 0,
5312
- max: 0
5313
- },
5314
- run: async (invocation) => {
5315
- const run = await runOf(adaptersOf, invocation);
5316
- const projectId = await projectFrom(run, invocation.values.project);
5317
- const items = await toolsOf(run.api, projectId);
5318
- return printed(run, items, (tools) => tools.length === 0 ? "The project offers no tool yet." : renderTable([
5319
- { header: "SLUG" },
5320
- { header: "NAME" },
5321
- { header: "KIND" },
5322
- { header: "SECRETS" },
5323
- {
5324
- header: "AGENTS",
5325
- align: "right"
5326
- },
5327
- { header: "ID" }
5328
- ], tools.map((tool) => [
5329
- tool.slug,
5330
- tool.name,
5331
- tool.kind,
5332
- tool.complete ? "complete" : "incomplete",
5333
- String(tool.agentsUsing),
5334
- tool.id
5335
- ])));
5336
- }
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()
5337
6271
  }),
5338
- defineCommand({
5339
- name: "show",
5340
- summary: "Show one tool with its configuration.",
5341
- usage: "tools show <tool> --project <name>",
5342
- optionsUsage: projectUsage,
5343
- options: projectOption,
5344
- positionals: {
5345
- min: 1,
5346
- max: 1
5347
- },
5348
- run: async (invocation) => {
5349
- const run = await runOf(adaptersOf, invocation);
5350
- const projectId = await projectFrom(run, invocation.values.project);
5351
- const tool = await resolveTool(run.api, projectId, invocation.positionals[0] ?? "");
5352
- return printed(run, tool, (shown) => [
5353
- renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
5354
- ["name", shown.name],
5355
- ["slug", shown.slug],
5356
- ["kind", shown.kind],
5357
- ["version", String(shown.version)],
5358
- ["complete", shown.complete ? "yes" : "no"],
5359
- ["agents", String(shown.agentsUsing)],
5360
- ["id", shown.id]
5361
- ]),
5362
- "",
5363
- JSON.stringify(shown.config, null, 2)
5364
- ].join("\n"));
5365
- }
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.")
5366
6287
  }),
5367
- defineCommand({
5368
- name: "create",
5369
- summary: "Create a tool from a JSON description of it.",
5370
- usage: "tools create --project <name> --file <path>",
5371
- optionsUsage: [...projectUsage, ...fileUsage("the tool")],
5372
- options: {
5373
- ...projectOption,
5374
- ...fileOptions
5375
- },
5376
- positionals: {
5377
- min: 0,
5378
- max: 0
5379
- },
5380
- run: async (invocation) => {
5381
- const run = await runOf(adaptersOf, invocation);
5382
- const projectId = await projectFrom(run, invocation.values.project);
5383
- const file = invocation.values.file;
5384
- if (file === void 0) throw new UsageError("tools create needs --file <path>.", "kcloud tools show <tool> --json prints the shape of a tool.");
5385
- const document = await documentOf(run, file);
5386
- validated(createToolBodySchema, document, "a tool");
5387
- const body = document;
5388
- const tool = dataOf("create the tool", await run.api.POST("/projects/{id}/tools", {
5389
- params: { path: { id: projectId } },
5390
- body
5391
- }), hints);
5392
- 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 }
5393
6293
  }
5394
- }),
5395
- defineCommand({
5396
- name: "update",
5397
- summary: "Change a tool from a JSON description of the change.",
5398
- usage: "tools update <tool> --project <name> --file <path>",
5399
- optionsUsage: [...projectUsage, ...fileUsage("the change")],
5400
- options: {
5401
- ...projectOption,
5402
- ...fileOptions
5403
- },
5404
- positionals: {
5405
- min: 1,
5406
- max: 1
5407
- },
5408
- run: async (invocation) => {
5409
- const run = await runOf(adaptersOf, invocation);
5410
- const projectId = await projectFrom(run, invocation.values.project);
5411
- const tool = await resolveTool(run.api, projectId, invocation.positionals[0] ?? "");
5412
- const file = invocation.values.file;
5413
- 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.");
5414
- const document = await documentOf(run, file);
5415
- const change = typeof document === "object" && document !== null && "version" in document ? document : {
5416
- ...document,
5417
- 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
5418
6397
  };
5419
- validated(updateToolBodySchema, change, "a change to a tool");
5420
- const body = change;
5421
- const updated = dataOf("change the tool", await run.api.PATCH("/projects/{id}/tools/{toolId}", {
5422
- params: { path: {
5423
- id: projectId,
5424
- toolId: tool.id
5425
- } },
5426
- body
5427
- }), hints);
5428
- return printed(run, updated, (shown) => `Changed ${shown.name}.`);
5429
6398
  }
5430
- }),
5431
- defineCommand({
5432
- name: "delete",
5433
- summary: "Delete a tool no agent uses.",
5434
- usage: "tools delete <tool> --project <name>",
5435
- optionsUsage: projectUsage,
5436
- options: projectOption,
5437
- positionals: {
5438
- min: 1,
5439
- max: 1
5440
- },
5441
- run: async (invocation) => {
5442
- const run = await runOf(adaptersOf, invocation);
5443
- const projectId = await projectFrom(run, invocation.values.project);
5444
- const reference = invocation.positionals[0] ?? "";
5445
- const tool = await resolveTool(run.api, projectId, reference);
5446
- await confirmed({
5447
- prompt: run.prompt,
5448
- settings: run.settings,
5449
- question: `Delete the tool ${tool.name}?`
5450
- });
5451
- doneWith("delete the tool", await run.api.DELETE("/projects/{id}/tools/{toolId}", { params: { path: {
5452
- id: projectId,
5453
- toolId: tool.id
5454
- } } }), hints);
5455
- 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));
5456
6403
  }
5457
- })
5458
- ]
6404
+ });
6405
+ await finished(stdin, { writable: false }).catch(() => void 0);
6406
+ await connection.close();
6407
+ return exitCodes.ok;
6408
+ }
5459
6409
  });
5460
6410
  //#endregion
5461
- //#region src/commands/tokens.ts
5462
- const lifetimeOf = (given) => {
5463
- if (given === void 0) return null;
5464
- const days = Number(given);
5465
- 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.`);
5466
- 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
+ }
5467
6422
  };
5468
- const createTokensCommand = (adaptersOf) => defineGroup({
5469
- name: "tokens",
5470
- summary: "The access tokens of your account on this instance.",
5471
- usage: "tokens <command>",
6423
+ const createProfilesCommand = () => defineGroup({
6424
+ name: "profiles",
6425
+ summary: "The instances this machine is signed in to.",
6426
+ usage: "profiles <command>",
5472
6427
  subcommands: [
5473
6428
  defineCommand({
5474
6429
  name: "list",
5475
- summary: "List the access tokens of your account.",
5476
- usage: "tokens list",
6430
+ summary: "List the saved profiles, marking the current one.",
6431
+ usage: "profiles list",
5477
6432
  options: {},
5478
6433
  positionals: {
5479
6434
  min: 0,
5480
6435
  max: 0
5481
6436
  },
5482
6437
  run: async ({ settings, context }) => {
5483
- const adapters = adaptersOf(context);
5484
- const session = await openSession({
5485
- settings,
5486
- context,
5487
- api: adapters.api
5488
- });
5489
- const { items } = dataOf("list the access tokens", await session.api.GET("/auth/me/tokens"));
6438
+ const store = await readProfileStore(context.homeDirectory);
5490
6439
  if (settings.json) {
5491
- 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
+ });
5492
6448
  return exitCodes.ok;
5493
6449
  }
5494
- context.terminal.line(items.length === 0 ? "Your account holds no access token on this instance." : renderTable([
5495
- { header: "ID" },
5496
- { header: "NAME" },
5497
- { header: "LAST USED" },
5498
- { header: "EXPIRES" }
5499
- ], items.map((token) => [
5500
- token.id,
5501
- token.name,
5502
- token.lastUsedAt ?? "never",
5503
- token.expiresAt ?? "never"
5504
- ])));
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));
5505
6456
  return exitCodes.ok;
5506
6457
  }
5507
6458
  }),
5508
6459
  defineCommand({
5509
- name: "create",
5510
- summary: "Create an access token and print its secret once.",
5511
- usage: "tokens create <name> [--expires-in <days>]",
5512
- optionsUsage: [["--expires-in <days>", `how long the token lives, from 1 to 365 days (default: forever)`]],
5513
- 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: {},
5514
6464
  positionals: {
5515
6465
  min: 1,
5516
6466
  max: 1
5517
6467
  },
5518
- run: async ({ values, positionals, settings, context }) => {
5519
- const adapters = adaptersOf(context);
5520
- const expiresInDays = lifetimeOf(values["expires-in"]);
5521
- const session = await openSession({
5522
- settings,
5523
- context,
5524
- 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
5525
6475
  });
5526
- const created = dataOf("create an access token", await session.api.POST("/auth/me/tokens", { body: {
5527
- name: positionals[0] ?? "",
5528
- expiresInDays
5529
- } }));
5530
- if (settings.json) {
5531
- context.terminal.json(created);
5532
- return exitCodes.ok;
5533
- }
5534
- context.terminal.line(created.secret);
5535
- 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.`);
5536
6477
  return exitCodes.ok;
5537
6478
  }
5538
6479
  }),
5539
6480
  defineCommand({
5540
- name: "revoke",
5541
- summary: "Revoke one access token of your account.",
5542
- usage: "tokens revoke <id>",
6481
+ name: "remove",
6482
+ summary: "Forget a saved profile without revoking its token.",
6483
+ usage: "profiles remove <name>",
5543
6484
  options: {},
5544
6485
  positionals: {
5545
6486
  min: 1,
5546
6487
  max: 1
5547
6488
  },
5548
- run: async ({ positionals, settings, context }) => {
5549
- const adapters = adaptersOf(context);
5550
- const id = positionals[0] ?? "";
5551
- const session = await openSession({
5552
- settings,
5553
- context,
5554
- api: adapters.api
5555
- });
5556
- await confirmed({
5557
- prompt: adapters.prompter(settings),
5558
- settings,
5559
- question: `Revoke the token ${id}? Everything using it stops working.`
5560
- });
5561
- 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." });
5562
- 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.`);
5563
6495
  return exitCodes.ok;
5564
6496
  }
5565
6497
  })
5566
6498
  ]
5567
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
+ });
5568
6532
  const cliVersion = createRequire(import.meta.url)("../package.json").version;
5569
6533
  //#endregion
5570
6534
  //#region src/commands.ts
@@ -5582,7 +6546,8 @@ const kcloudCommands = () => [
5582
6546
  createToolsCommand(createAdapters),
5583
6547
  createAgentsCommand(createAdapters),
5584
6548
  createNotificationsCommand(createAdapters),
5585
- createReleasesCommand(createAdapters)
6549
+ createReleasesCommand(createAdapters),
6550
+ createMcpCommand(createAdapters, cliVersion)
5586
6551
  ];
5587
6552
  //#endregion
5588
6553
  //#region src/main.ts