@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.
- package/README.md +7 -1
- package/dist/main.js +1416 -451
- 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
|
|
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
|
-
}) :
|
|
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
|
|
3310
|
-
id,
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
3778
|
-
|
|
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,
|
|
3882
|
+
optionsUsage: [...agentUsage, allUsage],
|
|
3842
3883
|
options: {
|
|
3843
3884
|
...agentOption,
|
|
3844
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 } } }),
|
|
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
|
-
}),
|
|
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
|
-
}),
|
|
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
|
-
}),
|
|
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
|
-
} } }),
|
|
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
|
-
} } }),
|
|
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 } } }),
|
|
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
|
-
|
|
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/
|
|
4850
|
-
const
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
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
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
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: "
|
|
4899
|
-
summary: "
|
|
4900
|
-
usage: "
|
|
4901
|
-
|
|
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 (
|
|
4907
|
-
const
|
|
4908
|
-
const
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
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: "
|
|
4920
|
-
summary: "
|
|
4921
|
-
usage: "
|
|
4922
|
-
|
|
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 (
|
|
4928
|
-
const
|
|
4929
|
-
const
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
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/
|
|
5030
|
-
const
|
|
5031
|
-
name: "
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
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
|
|
5063
|
-
const projectUsage
|
|
5064
|
-
const
|
|
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
|
|
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 } } }),
|
|
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
|
|
5102
|
-
options: projectOption
|
|
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
|
|
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
|
|
6011
|
+
optionsUsage: [...projectUsage, ["--credential <label>", "the github_pat credential to look through"]],
|
|
5133
6012
|
options: {
|
|
5134
|
-
...projectOption
|
|
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
|
|
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
|
-
} }),
|
|
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
|
|
6045
|
+
optionsUsage: [...projectUsage, ["--credential <label>", "the github_pat credential that can see it"]],
|
|
5167
6046
|
options: {
|
|
5168
|
-
...projectOption
|
|
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
|
|
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
|
-
}),
|
|
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
|
|
6076
|
+
optionsUsage: [...projectUsage, ["--credential <label>", "the github_pat credential to use from now on"]],
|
|
5198
6077
|
options: {
|
|
5199
|
-
...projectOption
|
|
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
|
|
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
|
-
}),
|
|
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
|
|
5228
|
-
options: projectOption
|
|
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
|
|
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
|
-
} } }),
|
|
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
|
|
5249
|
-
options: projectOption
|
|
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
|
|
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
|
-
} } }),
|
|
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/
|
|
5275
|
-
const
|
|
5276
|
-
const
|
|
5277
|
-
const
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
}
|
|
5290
|
-
const
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
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
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
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
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
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
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
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
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
},
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
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
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
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/
|
|
5462
|
-
const
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
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
|
|
5469
|
-
name: "
|
|
5470
|
-
summary: "The
|
|
5471
|
-
usage: "
|
|
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
|
|
5476
|
-
usage: "
|
|
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
|
|
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(
|
|
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
|
-
|
|
5495
|
-
|
|
5496
|
-
{ header: "
|
|
5497
|
-
{ header: "
|
|
5498
|
-
{ header: "
|
|
5499
|
-
],
|
|
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: "
|
|
5510
|
-
summary: "
|
|
5511
|
-
usage: "
|
|
5512
|
-
|
|
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 ({
|
|
5519
|
-
const
|
|
5520
|
-
const
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
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
|
-
|
|
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: "
|
|
5541
|
-
summary: "
|
|
5542
|
-
usage: "
|
|
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,
|
|
5549
|
-
const
|
|
5550
|
-
const
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
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
|