@plune-ai/cli 0.33.0 → 0.34.1
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 +28 -1
- package/dist/cli.cjs +1191 -131
- package/dist/index.cjs +2 -1
- package/dist/index.js +2 -1
- package/dist/mcp-tools.cjs +932 -0
- package/dist/mcp-tools.d.cts +894 -0
- package/dist/mcp-tools.d.ts +894 -0
- package/dist/mcp-tools.js +895 -0
- package/package.json +8 -2
package/dist/cli.cjs
CHANGED
|
@@ -1672,18 +1672,18 @@ var init_judge = __esm({
|
|
|
1672
1672
|
});
|
|
1673
1673
|
|
|
1674
1674
|
// src/orchestrator/concurrency.ts
|
|
1675
|
-
async function mapLimit(
|
|
1676
|
-
const results = new Array(
|
|
1675
|
+
async function mapLimit(items2, limit, fn) {
|
|
1676
|
+
const results = new Array(items2.length);
|
|
1677
1677
|
let next = 0;
|
|
1678
1678
|
const worker = async () => {
|
|
1679
|
-
while (next <
|
|
1679
|
+
while (next < items2.length) {
|
|
1680
1680
|
const i = next;
|
|
1681
1681
|
next += 1;
|
|
1682
|
-
results[i] = await fn(
|
|
1682
|
+
results[i] = await fn(items2[i], i);
|
|
1683
1683
|
}
|
|
1684
1684
|
};
|
|
1685
1685
|
const safeLimit = Number.isFinite(limit) && limit >= 1 ? Math.floor(limit) : 1;
|
|
1686
|
-
const workerCount = Math.min(safeLimit,
|
|
1686
|
+
const workerCount = Math.min(safeLimit, items2.length);
|
|
1687
1687
|
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
1688
1688
|
return results;
|
|
1689
1689
|
}
|
|
@@ -1991,7 +1991,7 @@ async function runOrchestration(config, options, deps) {
|
|
|
1991
1991
|
throw new RunConfigError("--no-cache and --cache-max-age cannot be used together");
|
|
1992
1992
|
}
|
|
1993
1993
|
const started = deps.now();
|
|
1994
|
-
const
|
|
1994
|
+
const dryRun2 = options.dryRun ?? false;
|
|
1995
1995
|
const evalResults = [];
|
|
1996
1996
|
const prepared = selectEvals(config.evals, options.only).map((ev) => {
|
|
1997
1997
|
const template = resolveTemplate(ev, deps.baseDir);
|
|
@@ -2010,14 +2010,15 @@ async function runOrchestration(config, options, deps) {
|
|
|
2010
2010
|
limit,
|
|
2011
2011
|
(row) => runRow({
|
|
2012
2012
|
template,
|
|
2013
|
-
|
|
2013
|
+
// `defaults.assertions` apply to every eval, ahead of its own (#2415).
|
|
2014
|
+
assertions: [...config.defaults?.assertions ?? [], ...ev.assertions],
|
|
2014
2015
|
row,
|
|
2015
2016
|
providerConfig,
|
|
2016
2017
|
provider,
|
|
2017
2018
|
embedder: deps.embedder,
|
|
2018
2019
|
cache: deps.cache,
|
|
2019
2020
|
now: deps.now,
|
|
2020
|
-
dryRun,
|
|
2021
|
+
dryRun: dryRun2,
|
|
2021
2022
|
noCache,
|
|
2022
2023
|
cacheStrict,
|
|
2023
2024
|
...options.cacheMaxAgeMs !== void 0 ? { cacheMaxAgeMs: options.cacheMaxAgeMs } : {}
|
|
@@ -2143,18 +2144,18 @@ __export(run_exports, {
|
|
|
2143
2144
|
function isMockMode(env) {
|
|
2144
2145
|
return env["PLUNE_MOCK_PROVIDER"] === "1";
|
|
2145
2146
|
}
|
|
2146
|
-
function resolveProviderFor(config,
|
|
2147
|
+
function resolveProviderFor(config, dryRun2) {
|
|
2147
2148
|
if (isMockMode(process.env)) return () => makeMockProvider();
|
|
2148
|
-
if (
|
|
2149
|
+
if (dryRun2) return (cfg) => makeEstimateProvider(cfg, config.pricing);
|
|
2149
2150
|
return (cfg) => getProvider(cfg, process.env, config.pricing);
|
|
2150
2151
|
}
|
|
2151
|
-
function buildRealDeps(config, baseDir,
|
|
2152
|
+
function buildRealDeps(config, baseDir, dryRun2) {
|
|
2152
2153
|
const dir = path5.join(baseDir, ".plune");
|
|
2153
2154
|
fs5.mkdirSync(dir, { recursive: true });
|
|
2154
2155
|
return {
|
|
2155
|
-
resolveProvider: resolveProviderFor(config,
|
|
2156
|
+
resolveProvider: resolveProviderFor(config, dryRun2),
|
|
2156
2157
|
embedder: getDefaultEmbedder(),
|
|
2157
|
-
cache:
|
|
2158
|
+
cache: dryRun2 ? NOOP_CACHE : openCache(path5.join(dir, "cache.db")),
|
|
2158
2159
|
now: Date.now,
|
|
2159
2160
|
loadDataset,
|
|
2160
2161
|
baseDir
|
|
@@ -2225,8 +2226,8 @@ function wrap(code, s) {
|
|
|
2225
2226
|
}
|
|
2226
2227
|
function createStyler(color) {
|
|
2227
2228
|
if (!color) {
|
|
2228
|
-
const
|
|
2229
|
-
return { green:
|
|
2229
|
+
const id2 = (s) => s;
|
|
2230
|
+
return { green: id2, red: id2, yellow: id2, dim: id2, bold: id2 };
|
|
2230
2231
|
}
|
|
2231
2232
|
return {
|
|
2232
2233
|
green: (s) => wrap(CODE.green, s),
|
|
@@ -2596,7 +2597,7 @@ function createClient(opts) {
|
|
|
2596
2597
|
}
|
|
2597
2598
|
const base = opts.apiUrl.replace(/\/+$/, "");
|
|
2598
2599
|
const scrub = (text2) => opts.token === "" ? text2 : text2.split(opts.token).join("***");
|
|
2599
|
-
async function send(method,
|
|
2600
|
+
async function send(method, path22, payload, retries = DEFAULT_RETRIES) {
|
|
2600
2601
|
let last = {
|
|
2601
2602
|
kind: "unavailable",
|
|
2602
2603
|
status: null,
|
|
@@ -2607,7 +2608,7 @@ function createClient(opts) {
|
|
|
2607
2608
|
await respectPause(mine);
|
|
2608
2609
|
let res;
|
|
2609
2610
|
try {
|
|
2610
|
-
res = await doFetch(`${base}${
|
|
2611
|
+
res = await doFetch(`${base}${path22}`, {
|
|
2611
2612
|
method,
|
|
2612
2613
|
headers: { "content-type": payload?.type ?? "application/json", authorization: `Bearer ${opts.token}` },
|
|
2613
2614
|
// A DELETE carries none. Sending `"undefined"` as a body is what a naive
|
|
@@ -2646,10 +2647,10 @@ function createClient(opts) {
|
|
|
2646
2647
|
return { ok: false, ...last };
|
|
2647
2648
|
}
|
|
2648
2649
|
return {
|
|
2649
|
-
post: (
|
|
2650
|
-
upload: (
|
|
2651
|
-
del: (
|
|
2652
|
-
get: (
|
|
2650
|
+
post: (path22, body, retries) => send("POST", path22, { body: JSON.stringify(body), type: "application/json" }, retries),
|
|
2651
|
+
upload: (path22, bytes, contentType) => send("POST", path22, { body: bytes, type: contentType }, uploadRetries ?? DEFAULT_RETRIES),
|
|
2652
|
+
del: (path22) => send("DELETE", path22),
|
|
2653
|
+
get: (path22) => send("GET", path22),
|
|
2653
2654
|
async put(url, headers, source) {
|
|
2654
2655
|
const putRetries = uploadRetries ?? 3;
|
|
2655
2656
|
let last = { ok: false, status: null, code: "network" };
|
|
@@ -2763,8 +2764,8 @@ function bounded(env, name, range, warnings) {
|
|
|
2763
2764
|
function list(env, name) {
|
|
2764
2765
|
const raw = value(env, name);
|
|
2765
2766
|
if (raw === void 0) return void 0;
|
|
2766
|
-
const
|
|
2767
|
-
return
|
|
2767
|
+
const items2 = raw.split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
2768
|
+
return items2.length === 0 ? void 0 : items2;
|
|
2768
2769
|
}
|
|
2769
2770
|
function readEnv(env = process.env) {
|
|
2770
2771
|
const externalKey = value(env, "PLUNE_RUN");
|
|
@@ -2887,13 +2888,13 @@ var init_fallback = __esm({
|
|
|
2887
2888
|
function failureOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
2888
2889
|
const error = chosen(attempt);
|
|
2889
2890
|
const line = error === void 0 ? void 0 : firstLine(machineless(error.text, attempt.repoRoot, home));
|
|
2890
|
-
const
|
|
2891
|
+
const headline2 = line !== void 0 && jsonBytes(line) <= HEADLINE_LIMIT ? line : void 0;
|
|
2891
2892
|
const steps = chainOf(attempt.steps);
|
|
2892
2893
|
const location = error === void 0 ? void 0 : fromRoot(placeOf(error, attempt.testFile), attempt.repoRoot);
|
|
2893
2894
|
const artifacts = attempt.attachments.filter((a) => a.path !== void 0 && !a.name.startsWith("_") && a.contentType !== METADATA_TYPE).map(({ name, contentType }) => ({ name: fileNameOf(name), ...contentType ? { contentType } : {} })).filter((a) => a.name !== "");
|
|
2894
2895
|
const ciUrl = webLink(attempt.buildHref);
|
|
2895
2896
|
const failure = {
|
|
2896
|
-
...
|
|
2897
|
+
...headline2 !== void 0 ? { headline: headline2 } : {},
|
|
2897
2898
|
...steps.length > 0 ? { steps } : {},
|
|
2898
2899
|
...location !== void 0 ? { location } : {},
|
|
2899
2900
|
...artifacts.length > 0 ? { artifacts } : {},
|
|
@@ -2930,12 +2931,12 @@ function machineless(raw, repoRoot, home) {
|
|
|
2930
2931
|
if (repoRoot === void 0) text2 = text2.replace(ANY_HOME, "~");
|
|
2931
2932
|
return text2;
|
|
2932
2933
|
}
|
|
2933
|
-
function pathIn(
|
|
2934
|
-
const parts =
|
|
2934
|
+
function pathIn(path22) {
|
|
2935
|
+
const parts = path22.split(/[\\/]+/);
|
|
2935
2936
|
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
2936
2937
|
if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
|
|
2937
2938
|
const joined = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
|
|
2938
|
-
const escaped = /^\/(?!\/)/.test(
|
|
2939
|
+
const escaped = /^\/(?!\/)/.test(path22) ? joined.replace("[\\\\/]+", "\\\\*\\/") : joined;
|
|
2939
2940
|
const url = "(?:(?:file|webpack|webpack-internal):(?:\\\\*\\/){2,3})?";
|
|
2940
2941
|
return new RegExp(`${url}${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
|
|
2941
2942
|
}
|
|
@@ -2981,12 +2982,12 @@ function placeOf(error, testFile) {
|
|
|
2981
2982
|
}
|
|
2982
2983
|
function fileOf(raw) {
|
|
2983
2984
|
if (!raw.startsWith("file://")) return raw;
|
|
2984
|
-
let
|
|
2985
|
+
let path22 = raw.slice("file://".length);
|
|
2985
2986
|
try {
|
|
2986
|
-
|
|
2987
|
+
path22 = decodeURIComponent(path22);
|
|
2987
2988
|
} catch {
|
|
2988
2989
|
}
|
|
2989
|
-
return /^\/[A-Za-z]:\//.test(
|
|
2990
|
+
return /^\/[A-Za-z]:\//.test(path22) ? path22.slice(1) : path22;
|
|
2990
2991
|
}
|
|
2991
2992
|
function fromRoot(place, root) {
|
|
2992
2993
|
if (place === void 0 || root === void 0 || !Number.isInteger(place.line) || place.line < 1) return void 0;
|
|
@@ -3026,14 +3027,14 @@ var init_failure_detail = __esm({
|
|
|
3026
3027
|
FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
3027
3028
|
FRAME_MAX = 8192;
|
|
3028
3029
|
firstLine = (text2) => (0, import_node_util.stripVTControlCharacters)(text2).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
3029
|
-
normal = (
|
|
3030
|
+
normal = (path22) => import_node_path2.posix.normalize(path22.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
3030
3031
|
}
|
|
3031
3032
|
});
|
|
3032
3033
|
|
|
3033
3034
|
// src/reporter-core/files-batch.ts
|
|
3034
|
-
async function sha256File(
|
|
3035
|
+
async function sha256File(path22) {
|
|
3035
3036
|
const hash = (0, import_node_crypto2.createHash)("sha256");
|
|
3036
|
-
for await (const part of (0, import_node_fs4.createReadStream)(
|
|
3037
|
+
for await (const part of (0, import_node_fs4.createReadStream)(path22)) hash.update(part);
|
|
3037
3038
|
return hash.digest("hex");
|
|
3038
3039
|
}
|
|
3039
3040
|
var import_node_crypto2, import_node_fs4, FILES_BATCH_MAX, DIRECT_BATCH_BYTES, BATCH_PIPELINES, sha256Bytes, BUCKET_CODES;
|
|
@@ -3140,9 +3141,9 @@ function packed(results) {
|
|
|
3140
3141
|
if (part.length > 0) out.push(part);
|
|
3141
3142
|
return out;
|
|
3142
3143
|
}
|
|
3143
|
-
function chunk(
|
|
3144
|
+
function chunk(items2, size) {
|
|
3144
3145
|
const out = [];
|
|
3145
|
-
for (let i = 0; i <
|
|
3146
|
+
for (let i = 0; i < items2.length; i += size) out.push(items2.slice(i, i + size));
|
|
3146
3147
|
return out;
|
|
3147
3148
|
}
|
|
3148
3149
|
function warnIfDropped(sent, run, log) {
|
|
@@ -3645,15 +3646,15 @@ async function startRun(passed, declared2) {
|
|
|
3645
3646
|
await complete(toComplete);
|
|
3646
3647
|
await sendDirect(directs);
|
|
3647
3648
|
}
|
|
3648
|
-
async function complete(
|
|
3649
|
-
for (let round = 0;
|
|
3649
|
+
async function complete(items2) {
|
|
3650
|
+
for (let round = 0; items2.length > 0; round += 1) {
|
|
3650
3651
|
const out = await gated(() => {
|
|
3651
3652
|
requests += 1;
|
|
3652
|
-
return client.post("/v1/files/complete-batch", { ids:
|
|
3653
|
+
return client.post("/v1/files/complete-batch", { ids: items2.map((i) => i.id) }, uploadRetries);
|
|
3653
3654
|
});
|
|
3654
3655
|
if (routeMissing(out)) {
|
|
3655
3656
|
await Promise.all(
|
|
3656
|
-
|
|
3657
|
+
items2.map(
|
|
3657
3658
|
(i) => gated(async () => {
|
|
3658
3659
|
requests += 1;
|
|
3659
3660
|
const one = await client.post(`/v1/files/${encodeURIComponent(i.id)}/complete`, {}, uploadRetries);
|
|
@@ -3665,28 +3666,28 @@ async function startRun(passed, declared2) {
|
|
|
3665
3666
|
return;
|
|
3666
3667
|
}
|
|
3667
3668
|
const verdicts = out.ok ? out.body?.items : void 0;
|
|
3668
|
-
if (!out.ok || !Array.isArray(verdicts) || verdicts.length !==
|
|
3669
|
-
for (const i of
|
|
3669
|
+
if (!out.ok || !Array.isArray(verdicts) || verdicts.length !== items2.length) {
|
|
3670
|
+
for (const i of items2) failedToBucket(i.job, out.ok ? "bad_answer" : codeOf2(out));
|
|
3670
3671
|
return;
|
|
3671
3672
|
}
|
|
3672
3673
|
const again = [];
|
|
3673
|
-
|
|
3674
|
+
items2.forEach((i, at) => {
|
|
3674
3675
|
const v = verdicts[at] ?? {};
|
|
3675
3676
|
if (v.status === "ready") countsOf(i.job.kind).uploaded += 1;
|
|
3676
3677
|
else if (v.retryable === true && round < Math.min(uploadRetries, 3)) again.push(i);
|
|
3677
3678
|
else refusedItem(i.job, 0, v.status === "refused" ? v : { code: "bad_answer" });
|
|
3678
3679
|
});
|
|
3679
|
-
|
|
3680
|
-
if (
|
|
3681
|
-
retried +=
|
|
3680
|
+
items2 = again;
|
|
3681
|
+
if (items2.length > 0) {
|
|
3682
|
+
retried += items2.length;
|
|
3682
3683
|
await itemPause(round);
|
|
3683
3684
|
}
|
|
3684
3685
|
}
|
|
3685
3686
|
}
|
|
3686
|
-
async function sendDirect(
|
|
3687
|
+
async function sendDirect(items2) {
|
|
3687
3688
|
const groups = [];
|
|
3688
3689
|
let bytes = 0;
|
|
3689
|
-
for (const item of
|
|
3690
|
+
for (const item of items2) {
|
|
3690
3691
|
const group = groups[groups.length - 1];
|
|
3691
3692
|
if (group === void 0 || group.length >= FILES_BATCH_MAX || bytes + item.size > DIRECT_BATCH_BYTES) {
|
|
3692
3693
|
groups.push([item]);
|
|
@@ -3752,10 +3753,10 @@ async function startRun(passed, declared2) {
|
|
|
3752
3753
|
})
|
|
3753
3754
|
);
|
|
3754
3755
|
}
|
|
3755
|
-
async function upload(
|
|
3756
|
-
if (filesByKey.size === 0 || !Array.isArray(
|
|
3756
|
+
async function upload(items2, filesByKey) {
|
|
3757
|
+
if (filesByKey.size === 0 || !Array.isArray(items2)) return;
|
|
3757
3758
|
const jobs = [];
|
|
3758
|
-
for (const item of
|
|
3759
|
+
for (const item of items2) {
|
|
3759
3760
|
if (item?.status !== "accepted" || typeof item.id !== "string" || typeof item.resultKey !== "string") continue;
|
|
3760
3761
|
for (const file of filesByKey.get(item.resultKey) ?? []) {
|
|
3761
3762
|
const type = mediaTypeOf(file.contentType) ?? (file.path === void 0 ? void 0 : TYPE_BY_EXTENSION[(0, import_node_path3.extname)(file.path).toLowerCase()]);
|
|
@@ -4110,9 +4111,9 @@ function connect(deps) {
|
|
|
4110
4111
|
`))
|
|
4111
4112
|
};
|
|
4112
4113
|
}
|
|
4113
|
-
function runUrl(apiUrl,
|
|
4114
|
+
function runUrl(apiUrl, id2) {
|
|
4114
4115
|
const web = dashboardUrl(apiUrl);
|
|
4115
|
-
return web === void 0 ? `${apiUrl}/v1/runs/${
|
|
4116
|
+
return web === void 0 ? `${apiUrl}/v1/runs/${id2}` : `${web}/runs/${id2}`;
|
|
4116
4117
|
}
|
|
4117
4118
|
async function handleRunStart(options = {}) {
|
|
4118
4119
|
const { client, apiUrl, write } = connect(options);
|
|
@@ -4140,39 +4141,39 @@ async function handleRunStart(options = {}) {
|
|
|
4140
4141
|
}
|
|
4141
4142
|
return result;
|
|
4142
4143
|
}
|
|
4143
|
-
async function handleRunFinish(
|
|
4144
|
+
async function handleRunFinish(id2, options = {}, runnerCode) {
|
|
4144
4145
|
const { client, write } = connect(options);
|
|
4145
4146
|
const event = options.terminate === true ? "terminate" : "finish";
|
|
4146
|
-
const out = await client.post(`/v1/runs/${
|
|
4147
|
+
const out = await client.post(`/v1/runs/${id2}/events`, {
|
|
4147
4148
|
event,
|
|
4148
4149
|
...options.reason !== void 0 ? { reason: options.reason } : {}
|
|
4149
4150
|
});
|
|
4150
4151
|
if (!out.ok) {
|
|
4151
4152
|
throw new RunCommandError(
|
|
4152
|
-
out.status === 404 ? `No run '${
|
|
4153
|
+
out.status === 404 ? `No run '${id2}' \u2014 check the id, or whether the token belongs to the same project.` : `Could not ${event} run ${id2} \u2014 ${out.detail || out.kind}.`
|
|
4153
4154
|
);
|
|
4154
4155
|
}
|
|
4155
|
-
write(`Run ${
|
|
4156
|
+
write(`Run ${id2} ${event === "finish" ? "finished" : "terminated"}.`);
|
|
4156
4157
|
if (options.failOnFailures !== true) return runnerCode ?? 0;
|
|
4157
4158
|
const summary = out.body?.summary;
|
|
4158
4159
|
const gate = gateExit({ statusEvent: out.body?.run?.statusEvent, counts: summary?.counts, notRun: summary?.notRun }, runnerCode);
|
|
4159
4160
|
write(gate.line);
|
|
4160
4161
|
return gate.code;
|
|
4161
4162
|
}
|
|
4162
|
-
async function handleRunDelete(
|
|
4163
|
+
async function handleRunDelete(id2, options = {}) {
|
|
4163
4164
|
const { client, write } = connect(options);
|
|
4164
|
-
const out = await client.del(`/v1/runs/${encodeURIComponent(
|
|
4165
|
+
const out = await client.del(`/v1/runs/${encodeURIComponent(id2)}`);
|
|
4165
4166
|
if (!out.ok) {
|
|
4166
4167
|
throw new RunCommandError(
|
|
4167
4168
|
out.status === 404 ? (
|
|
4168
4169
|
// Three causes, one answer, and the message names all three because the platform will not:
|
|
4169
4170
|
// a 404 that distinguished "not yours" from "not there" would tell a stranger which ids
|
|
4170
4171
|
// exist. The person holding the terminal is better served by the list than by a guess.
|
|
4171
|
-
`No run '${
|
|
4172
|
-
) : `Could not delete run ${
|
|
4172
|
+
`No run '${id2}' \u2014 check the id, whether the token belongs to the same project, or whether it is already deleted.`
|
|
4173
|
+
) : `Could not delete run ${id2} \u2014 ${out.detail || out.kind}.`
|
|
4173
4174
|
);
|
|
4174
4175
|
}
|
|
4175
|
-
write(`Run ${
|
|
4176
|
+
write(`Run ${id2} deleted, with its results and the review-queue entries it produced.`);
|
|
4176
4177
|
write("Recoverable for 30 days \u2014 an owner of the project can put it back from Settings \u2192 Bin.");
|
|
4177
4178
|
}
|
|
4178
4179
|
async function handleRunExec(options) {
|
|
@@ -4456,11 +4457,11 @@ var init_job_reporter = __esm({
|
|
|
4456
4457
|
}
|
|
4457
4458
|
async finish(outcome) {
|
|
4458
4459
|
if (this.id === void 0) return;
|
|
4459
|
-
const
|
|
4460
|
+
const id2 = this.id;
|
|
4460
4461
|
this.id = void 0;
|
|
4461
4462
|
await this.send(
|
|
4462
4463
|
"PATCH",
|
|
4463
|
-
`/${encodeURIComponent(
|
|
4464
|
+
`/${encodeURIComponent(id2)}/state`,
|
|
4464
4465
|
outcome.ok ? { state: "completed" } : { state: "failed", reason: outcome.reason.slice(0, JOB_REASON_MAX) }
|
|
4465
4466
|
);
|
|
4466
4467
|
}
|
|
@@ -4552,11 +4553,11 @@ async function handleSync(deps = {}) {
|
|
|
4552
4553
|
throw new SyncHttpError(res.status, detail);
|
|
4553
4554
|
}
|
|
4554
4555
|
const body = await res.json().catch(() => ({}));
|
|
4555
|
-
const
|
|
4556
|
-
const result = { id, url: `${endpoint}/${
|
|
4557
|
-
await job.uploaded({ cursor:
|
|
4556
|
+
const id2 = typeof body.id === "string" ? body.id : "";
|
|
4557
|
+
const result = { id: id2, url: `${endpoint}/${id2}` };
|
|
4558
|
+
await job.uploaded({ cursor: id2 || void 0 });
|
|
4558
4559
|
if (deps.failOnFailures === true) {
|
|
4559
|
-
result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${
|
|
4560
|
+
result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id2}/summary`, bearer));
|
|
4560
4561
|
}
|
|
4561
4562
|
return result;
|
|
4562
4563
|
}
|
|
@@ -4666,10 +4667,10 @@ function platformGet(deps) {
|
|
|
4666
4667
|
const token = (deps.loadToken ?? loadToken)();
|
|
4667
4668
|
if (token === null) throw new NotLoggedInError();
|
|
4668
4669
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
4669
|
-
return async (
|
|
4670
|
+
return async (path22) => {
|
|
4670
4671
|
let res;
|
|
4671
4672
|
try {
|
|
4672
|
-
res = await doFetch(`${apiUrl}${
|
|
4673
|
+
res = await doFetch(`${apiUrl}${path22}`, { headers: { authorization: `Bearer ${token}` } });
|
|
4673
4674
|
} catch {
|
|
4674
4675
|
throw new SyncNetworkError(apiUrl);
|
|
4675
4676
|
}
|
|
@@ -5024,9 +5025,9 @@ var init_read_file = __esm({
|
|
|
5024
5025
|
});
|
|
5025
5026
|
|
|
5026
5027
|
// src/importers/allure.ts
|
|
5027
|
-
function isFolder(
|
|
5028
|
+
function isFolder(path22) {
|
|
5028
5029
|
try {
|
|
5029
|
-
return fs12.statSync(
|
|
5030
|
+
return fs12.statSync(path22).isDirectory();
|
|
5030
5031
|
} catch {
|
|
5031
5032
|
return false;
|
|
5032
5033
|
}
|
|
@@ -5127,8 +5128,8 @@ function readAllure(dir, missing = [], notes = [], maxBytes = MAX_JSON_BYTES) {
|
|
|
5127
5128
|
const ctx = { folder, lexical, missing, notes, around, direct };
|
|
5128
5129
|
const groups = /* @__PURE__ */ new Map();
|
|
5129
5130
|
tests.forEach((r, i) => {
|
|
5130
|
-
const
|
|
5131
|
-
const g =
|
|
5131
|
+
const id2 = textOf(r.historyId);
|
|
5132
|
+
const g = id2 === "" ? `\0${i}` : id2;
|
|
5132
5133
|
const list2 = groups.get(g);
|
|
5133
5134
|
if (list2 === void 0) groups.set(g, [r]);
|
|
5134
5135
|
else list2.push(r);
|
|
@@ -5540,10 +5541,10 @@ function attemptOf(result, file, ctx) {
|
|
|
5540
5541
|
...typeof at?.file === "string" && typeof at.line === "number" ? { location: { file: at.file, line: at.line, ...typeof at.column === "number" ? { column: at.column } : {} } } : {}
|
|
5541
5542
|
})),
|
|
5542
5543
|
steps: steps(result.steps),
|
|
5543
|
-
attachments: (result.attachments ?? []).map(({ name, contentType, path:
|
|
5544
|
+
attachments: (result.attachments ?? []).map(({ name, contentType, path: path22, body }) => ({
|
|
5544
5545
|
name: name ?? "",
|
|
5545
5546
|
...contentType !== void 0 ? { contentType } : {},
|
|
5546
|
-
...
|
|
5547
|
+
...path22 !== void 0 ? { path: path22 } : {},
|
|
5547
5548
|
// Decoded only where the platform would take it (plune-ai/plune#928): a report of screenshots kept as
|
|
5548
5549
|
// bodies is megabytes of base64 that nothing here will read.
|
|
5549
5550
|
...typeof body === "string" && isTextType(contentType) ? { body: Buffer.from(body, "base64") } : {}
|
|
@@ -5783,25 +5784,25 @@ function readLighthouse(source, file, minScores = {}) {
|
|
|
5783
5784
|
const specRef = path14.basename(file);
|
|
5784
5785
|
const known = Object.entries(categories).filter(([, raw]) => isObject2(raw)).map(([key, raw]) => idOf(key, raw));
|
|
5785
5786
|
if (known.length > 0) {
|
|
5786
|
-
for (const [
|
|
5787
|
-
if (!known.includes(
|
|
5787
|
+
for (const [id2, score] of Object.entries(minScores)) {
|
|
5788
|
+
if (!known.includes(id2)) {
|
|
5788
5789
|
throw new JsonReportError(
|
|
5789
5790
|
file,
|
|
5790
|
-
`--min-score ${printable(
|
|
5791
|
+
`--min-score ${printable(id2)}=${score} names a category the report does not have; it has ${known.map(printable).join(", ")}.`
|
|
5791
5792
|
);
|
|
5792
5793
|
}
|
|
5793
5794
|
}
|
|
5794
5795
|
}
|
|
5795
5796
|
const out = [];
|
|
5796
|
-
const result = (
|
|
5797
|
-
const value2 = `${SOURCE5}#${url ?? ""}#${formFactor !== void 0 ? `${formFactor}#` : ""}${
|
|
5797
|
+
const result = (id2, passed, message) => {
|
|
5798
|
+
const value2 = `${SOURCE5}#${url ?? ""}#${formFactor !== void 0 ? `${formFactor}#` : ""}${id2}`;
|
|
5798
5799
|
const where = [url, formFactor].filter((part) => part !== void 0);
|
|
5799
5800
|
out.push({
|
|
5800
5801
|
resultKey: resultKey(value2, 0),
|
|
5801
5802
|
keys: [{ kind: "path-title", value: value2 }],
|
|
5802
5803
|
source: SOURCE5,
|
|
5803
5804
|
rawStatus: passed ? "passed" : "failed",
|
|
5804
|
-
title: where.length > 0 ? `${
|
|
5805
|
+
title: where.length > 0 ? `${id2} (${where.join(", ")})` : id2,
|
|
5805
5806
|
specRef,
|
|
5806
5807
|
...execution !== void 0 ? { execution } : {},
|
|
5807
5808
|
errorContext: message
|
|
@@ -5810,9 +5811,9 @@ function readLighthouse(source, file, minScores = {}) {
|
|
|
5810
5811
|
for (const [key, raw] of Object.entries(categories)) {
|
|
5811
5812
|
if (!isObject2(raw)) continue;
|
|
5812
5813
|
const category = raw;
|
|
5813
|
-
const
|
|
5814
|
-
const name = typeof category.title === "string" && category.title !== "" ? category.title :
|
|
5815
|
-
const bar = (Object.hasOwn(minScores,
|
|
5814
|
+
const id2 = idOf(key, category);
|
|
5815
|
+
const name = typeof category.title === "string" && category.title !== "" ? category.title : id2;
|
|
5816
|
+
const bar = (Object.hasOwn(minScores, id2) ? minScores[id2] : void 0) ?? DEFAULT_MIN_SCORE;
|
|
5816
5817
|
let passed = false;
|
|
5817
5818
|
let message;
|
|
5818
5819
|
if (runtimeError !== void 0) {
|
|
@@ -5824,7 +5825,7 @@ function readLighthouse(source, file, minScores = {}) {
|
|
|
5824
5825
|
passed = shown >= bar;
|
|
5825
5826
|
message = `Lighthouse ${name} score: ${shown}, ${passed ? "at least" : "below"} the ${bar} needed to pass.`;
|
|
5826
5827
|
}
|
|
5827
|
-
result(
|
|
5828
|
+
result(id2, passed, message);
|
|
5828
5829
|
}
|
|
5829
5830
|
if (out.length === 0 && runtimeError !== void 0) result("run", false, runtimeError);
|
|
5830
5831
|
if (out.length === 0) {
|
|
@@ -6103,9 +6104,9 @@ async function handleRunImport(options) {
|
|
|
6103
6104
|
for (const line of session.shardLines) write(line);
|
|
6104
6105
|
return result;
|
|
6105
6106
|
}
|
|
6106
|
-
function runUrl2(apiUrl,
|
|
6107
|
+
function runUrl2(apiUrl, id2) {
|
|
6107
6108
|
const web = dashboardUrl(apiUrl);
|
|
6108
|
-
return web === void 0 ? `${apiUrl}/v1/runs/${
|
|
6109
|
+
return web === void 0 ? `${apiUrl}/v1/runs/${id2}` : `${web}/runs/${id2}`;
|
|
6109
6110
|
}
|
|
6110
6111
|
function describe(result, apiUrl) {
|
|
6111
6112
|
const lines = [
|
|
@@ -6241,16 +6242,17 @@ function classify2(baseline, current) {
|
|
|
6241
6242
|
if (baseline === "absent") return current === "passed" ? "new-pass" : "new-fail";
|
|
6242
6243
|
if (current === "absent") return "removed";
|
|
6243
6244
|
if (baseline === "passed") return current === "passed" ? "stable-pass" : "regression";
|
|
6244
|
-
|
|
6245
|
+
if (current === "passed") return "improvement";
|
|
6246
|
+
return baseline === "errored" ? "unmeasured" : "pre-existing-fail";
|
|
6245
6247
|
}
|
|
6246
6248
|
function diffRuns(baseline, current) {
|
|
6247
6249
|
const baseMap = new Map(baseline.evals.map((e) => [e.id, evalStatus(e)]));
|
|
6248
6250
|
const curMap = new Map(current.evals.map((e) => [e.id, evalStatus(e)]));
|
|
6249
6251
|
const ids = [.../* @__PURE__ */ new Set([...baseMap.keys(), ...curMap.keys()])];
|
|
6250
|
-
const evals = ids.map((
|
|
6251
|
-
const baselineP = baseMap.get(
|
|
6252
|
-
const currentP = curMap.get(
|
|
6253
|
-
return { id, status: classify2(baselineP, currentP), baseline: baselineP, current: currentP };
|
|
6252
|
+
const evals = ids.map((id2) => {
|
|
6253
|
+
const baselineP = baseMap.get(id2) ?? "absent";
|
|
6254
|
+
const currentP = curMap.get(id2) ?? "absent";
|
|
6255
|
+
return { id: id2, status: classify2(baselineP, currentP), baseline: baselineP, current: currentP };
|
|
6254
6256
|
});
|
|
6255
6257
|
const count2 = (s) => evals.filter((e) => e.status === s).length;
|
|
6256
6258
|
const summary = {
|
|
@@ -6262,6 +6264,7 @@ function diffRuns(baseline, current) {
|
|
|
6262
6264
|
stablePasses: count2("stable-pass"),
|
|
6263
6265
|
errored: count2("errored"),
|
|
6264
6266
|
removed: count2("removed"),
|
|
6267
|
+
unmeasured: count2("errored") + count2("unmeasured"),
|
|
6265
6268
|
hasRegression: count2("regression") > 0
|
|
6266
6269
|
};
|
|
6267
6270
|
return { evals, summary };
|
|
@@ -6332,6 +6335,11 @@ __export(diff_exports2, {
|
|
|
6332
6335
|
renderDiff: () => renderDiff,
|
|
6333
6336
|
renderDiffMarkdown: () => renderDiffMarkdown
|
|
6334
6337
|
});
|
|
6338
|
+
function headline(d) {
|
|
6339
|
+
if (d.summary.hasRegression) return { mark: "\u274C", text: `${d.summary.regressions} regression(s)` };
|
|
6340
|
+
if (d.summary.unmeasured > 0) return { mark: "\u26A0\uFE0F", text: `${d.summary.unmeasured} eval(s) not measured` };
|
|
6341
|
+
return null;
|
|
6342
|
+
}
|
|
6335
6343
|
function summaryLine(d) {
|
|
6336
6344
|
const s = d.summary;
|
|
6337
6345
|
return [
|
|
@@ -6341,14 +6349,14 @@ function summaryLine(d) {
|
|
|
6341
6349
|
`${s.preExistingFails} pre-existing-fail`,
|
|
6342
6350
|
`${s.errored} errored`,
|
|
6343
6351
|
`${s.removed} removed`,
|
|
6352
|
+
`${s.unmeasured} not measured`,
|
|
6344
6353
|
`${s.stablePasses} stable`
|
|
6345
6354
|
].join(" \xB7 ");
|
|
6346
6355
|
}
|
|
6347
6356
|
function renderDiffMarkdown(d) {
|
|
6348
6357
|
const out = [STICKY_MARKER, "## Plune eval diff", ""];
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
);
|
|
6358
|
+
const head = headline(d);
|
|
6359
|
+
out.push(head ? `### ${head.mark} ${head.text}` : "### \u2705 No regressions");
|
|
6352
6360
|
out.push("");
|
|
6353
6361
|
const notable = d.evals.filter((e) => e.status !== "stable-pass");
|
|
6354
6362
|
if (notable.length > 0) {
|
|
@@ -6363,9 +6371,8 @@ function renderDiffMarkdown(d) {
|
|
|
6363
6371
|
return out.join("\n");
|
|
6364
6372
|
}
|
|
6365
6373
|
function renderDiffConsole(d) {
|
|
6366
|
-
const
|
|
6367
|
-
|
|
6368
|
-
];
|
|
6374
|
+
const head = headline(d);
|
|
6375
|
+
const out = [`Plune eval diff: ${head ? head.text : "no regressions"}`];
|
|
6369
6376
|
for (const e of d.evals) {
|
|
6370
6377
|
if (e.status === "stable-pass") continue;
|
|
6371
6378
|
out.push(` ${e.id}: ${e.baseline} -> ${e.current} [${e.status}]`);
|
|
@@ -6395,6 +6402,7 @@ var init_diff3 = __esm({
|
|
|
6395
6402
|
"new-pass": "\u{1F7E2} new pass",
|
|
6396
6403
|
"stable-pass": "\u2705 stable",
|
|
6397
6404
|
errored: "\u26A0\uFE0F errored",
|
|
6405
|
+
unmeasured: "\u26A0\uFE0F not measured (baseline errored)",
|
|
6398
6406
|
removed: "\u2796 removed"
|
|
6399
6407
|
};
|
|
6400
6408
|
}
|
|
@@ -6689,6 +6697,1033 @@ var init_logout = __esm({
|
|
|
6689
6697
|
}
|
|
6690
6698
|
});
|
|
6691
6699
|
|
|
6700
|
+
// src/mcp-tools/schemas.ts
|
|
6701
|
+
var import_zod4, BATCH_MAX2, isHttp, httpLink, id, dryRun, MISSION_STATES, MISSION_DOCUMENTATION, MISSION_LINK_KINDS, missionName, missionSummary, missionUrl, missionFields, sourceTitle, sourceVersion, linkSource, fileSource, ACCESS_KINDS, questionText, questionInput, STRATEGY_SOURCE_KINDS, TEST_LEVELS, QUALITY_CHARACTERISTICS, TEST_TECHNIQUES, RISK_SCALE, itemFields, strategyItem, items, unique, linkIds, share, strategyContent, SOURCE_REF_KINDS, proposedCase, API_METHODS, proposedEndpoint, REVIEW_STATES, REVIEW_KINDS, REJECTION_CATEGORIES, entity, plannedRequest, writeOutput, batchOutput, bulkPreview;
|
|
6702
|
+
var init_schemas = __esm({
|
|
6703
|
+
"src/mcp-tools/schemas.ts"() {
|
|
6704
|
+
"use strict";
|
|
6705
|
+
init_cjs_shims();
|
|
6706
|
+
import_zod4 = require("zod");
|
|
6707
|
+
BATCH_MAX2 = 100;
|
|
6708
|
+
isHttp = (s) => {
|
|
6709
|
+
try {
|
|
6710
|
+
const u = new URL(s);
|
|
6711
|
+
return u.protocol === "http:" || u.protocol === "https:";
|
|
6712
|
+
} catch {
|
|
6713
|
+
return false;
|
|
6714
|
+
}
|
|
6715
|
+
};
|
|
6716
|
+
httpLink = (what, max) => import_zod4.z.string().trim().min(1, `${what} is empty`).max(max).refine(isHttp, `${what} must be an http or https address`);
|
|
6717
|
+
id = (what) => import_zod4.z.string().min(1).describe(what);
|
|
6718
|
+
dryRun = import_zod4.z.boolean().optional().describe("true: answer what would be sent, and send nothing");
|
|
6719
|
+
MISSION_STATES = ["draft", "active", "done"];
|
|
6720
|
+
MISSION_DOCUMENTATION = ["unknown", "provided", "none"];
|
|
6721
|
+
MISSION_LINK_KINDS = ["url", "openapi", "repo", "ticket"];
|
|
6722
|
+
missionName = import_zod4.z.string().trim().min(1, "a mission needs a name").max(200);
|
|
6723
|
+
missionSummary = import_zod4.z.string().trim().max(2e3).describe("What the product is, in 3-5 sentences");
|
|
6724
|
+
missionUrl = httpLink("url", 2048).nullable().describe("Where the product runs; null takes it off");
|
|
6725
|
+
missionFields = {
|
|
6726
|
+
name: missionName,
|
|
6727
|
+
summary: missionSummary.optional(),
|
|
6728
|
+
url: missionUrl.optional(),
|
|
6729
|
+
state: import_zod4.z.enum(MISSION_STATES).optional(),
|
|
6730
|
+
documentation: import_zod4.z.enum(MISSION_DOCUMENTATION).optional().describe("Is there documentation: provided (it is among the sources), none, or unknown")
|
|
6731
|
+
};
|
|
6732
|
+
sourceTitle = import_zod4.z.string().trim().min(1).max(200);
|
|
6733
|
+
sourceVersion = import_zod4.z.string().trim().min(1).max(200).describe("A commit, a tag, a revision, an ETag");
|
|
6734
|
+
linkSource = import_zod4.z.strictObject({
|
|
6735
|
+
missionId: id("The mission"),
|
|
6736
|
+
kind: import_zod4.z.enum(MISSION_LINK_KINDS),
|
|
6737
|
+
ref: httpLink("ref", 2048).describe("The link"),
|
|
6738
|
+
title: sourceTitle.optional(),
|
|
6739
|
+
version: sourceVersion.optional(),
|
|
6740
|
+
dry_run: dryRun
|
|
6741
|
+
});
|
|
6742
|
+
fileSource = import_zod4.z.strictObject({
|
|
6743
|
+
missionId: id("The mission"),
|
|
6744
|
+
kind: import_zod4.z.literal("file"),
|
|
6745
|
+
name: import_zod4.z.string().trim().min(1).max(200).regex(/\.(txt|md|markdown|json)$/i, "a source file is .txt, .md, .markdown or .json").describe("The file name; its extension sets the type"),
|
|
6746
|
+
text: import_zod4.z.string().max(1048576).describe("The whole text of the file, UTF-8, at most 1 MiB"),
|
|
6747
|
+
title: sourceTitle.optional(),
|
|
6748
|
+
version: sourceVersion.optional(),
|
|
6749
|
+
dry_run: dryRun
|
|
6750
|
+
});
|
|
6751
|
+
ACCESS_KINDS = ["openapi", "repo", "staging", "har"];
|
|
6752
|
+
questionText = import_zod4.z.string().trim().min(1).max(2e3);
|
|
6753
|
+
questionInput = import_zod4.z.strictObject({
|
|
6754
|
+
template: import_zod4.z.enum(["plain", "access"]).describe("plain: a question with up to three options; access: ask for an API description, the code, a staging host or a HAR file"),
|
|
6755
|
+
text: questionText,
|
|
6756
|
+
missionId: import_zod4.z.string().min(1).optional(),
|
|
6757
|
+
options: import_zod4.z.array(import_zod4.z.string().trim().min(1).max(200)).min(1).max(3).refine((o) => new Set(o).size === o.length, { message: "no two options are the same" }).optional().describe("plain only: what the person may pick from"),
|
|
6758
|
+
access: import_zod4.z.array(import_zod4.z.enum(ACCESS_KINDS)).min(1).max(ACCESS_KINDS.length).refine((a) => new Set(a).size === a.length, { message: "an access kind is named once" }).optional().describe("access only: what you ask to be given"),
|
|
6759
|
+
dry_run: dryRun
|
|
6760
|
+
});
|
|
6761
|
+
STRATEGY_SOURCE_KINDS = ["mission-source", "doc-section", "screen", "endpoint", "requirement", "url"];
|
|
6762
|
+
TEST_LEVELS = ["component", "component-integration", "system", "system-integration", "acceptance"];
|
|
6763
|
+
QUALITY_CHARACTERISTICS = [
|
|
6764
|
+
"functional-suitability",
|
|
6765
|
+
"performance-efficiency",
|
|
6766
|
+
"compatibility",
|
|
6767
|
+
"interaction-capability",
|
|
6768
|
+
"reliability",
|
|
6769
|
+
"security",
|
|
6770
|
+
"maintainability",
|
|
6771
|
+
"flexibility",
|
|
6772
|
+
"safety"
|
|
6773
|
+
];
|
|
6774
|
+
TEST_TECHNIQUES = [
|
|
6775
|
+
"equivalence-partitioning",
|
|
6776
|
+
"boundary-value",
|
|
6777
|
+
"decision-table",
|
|
6778
|
+
"state-transition",
|
|
6779
|
+
"statement",
|
|
6780
|
+
"branch",
|
|
6781
|
+
"error-guessing",
|
|
6782
|
+
"exploratory",
|
|
6783
|
+
"checklist"
|
|
6784
|
+
];
|
|
6785
|
+
RISK_SCALE = ["low", "medium", "high"];
|
|
6786
|
+
itemFields = {
|
|
6787
|
+
text: import_zod4.z.string().trim().min(1).max(1e3).describe("Markdown"),
|
|
6788
|
+
source: import_zod4.z.strictObject({ kind: import_zod4.z.enum(STRATEGY_SOURCE_KINDS), ref: import_zod4.z.string().trim().min(1).max(300) }).optional().describe("Where it was read"),
|
|
6789
|
+
assumption: import_zod4.z.literal(true).optional().describe("You assumed it: a person confirms"),
|
|
6790
|
+
waitingOn: import_zod4.z.string().min(1).optional().describe("The id of an open question of this mission it waits for")
|
|
6791
|
+
};
|
|
6792
|
+
strategyItem = import_zod4.z.strictObject(itemFields);
|
|
6793
|
+
items = import_zod4.z.array(strategyItem).min(1).max(25);
|
|
6794
|
+
unique = (values) => import_zod4.z.array(import_zod4.z.enum(values)).min(1).refine((a) => new Set(a).size === a.length, { message: "named once" });
|
|
6795
|
+
linkIds = import_zod4.z.array(import_zod4.z.string().min(1)).max(50).refine((a) => new Set(a).size === a.length, { message: "an id is named once" });
|
|
6796
|
+
share = import_zod4.z.number().int().min(0).max(100);
|
|
6797
|
+
strategyContent = import_zod4.z.strictObject({
|
|
6798
|
+
objectives: items,
|
|
6799
|
+
scope: import_zod4.z.strictObject({ in: items, out: items.describe("What is deliberately not tested, and why") }),
|
|
6800
|
+
approach: items,
|
|
6801
|
+
profile: import_zod4.z.strictObject({
|
|
6802
|
+
levels: unique(TEST_LEVELS),
|
|
6803
|
+
characteristics: unique(QUALITY_CHARACTERISTICS),
|
|
6804
|
+
techniques: unique(TEST_TECHNIQUES),
|
|
6805
|
+
split: import_zod4.z.strictObject({ api: share, ui: share, manual: share }).refine((s) => s.api + s.ui + s.manual === 100, { message: "the split of api, ui and manual adds up to 100" }),
|
|
6806
|
+
notes: items
|
|
6807
|
+
}),
|
|
6808
|
+
risks: import_zod4.z.array(
|
|
6809
|
+
import_zod4.z.strictObject({
|
|
6810
|
+
...itemFields,
|
|
6811
|
+
likelihood: import_zod4.z.enum(RISK_SCALE),
|
|
6812
|
+
impact: import_zod4.z.enum(RISK_SCALE),
|
|
6813
|
+
caseIds: linkIds.optional(),
|
|
6814
|
+
requirementIds: linkIds.optional()
|
|
6815
|
+
})
|
|
6816
|
+
).min(1).max(50),
|
|
6817
|
+
entryCriteria: items,
|
|
6818
|
+
exitCriteria: items
|
|
6819
|
+
});
|
|
6820
|
+
SOURCE_REF_KINDS = ["url", "openapi-operation", "requirement", "pasted-text"];
|
|
6821
|
+
proposedCase = import_zod4.z.strictObject({
|
|
6822
|
+
title: import_zod4.z.string().trim().min(1).max(300),
|
|
6823
|
+
steps: import_zod4.z.array(import_zod4.z.string().trim().min(1).max(400)).min(1).max(20),
|
|
6824
|
+
expected: import_zod4.z.string().trim().min(1).max(2e3),
|
|
6825
|
+
execution: import_zod4.z.enum(["auto", "manual"]).optional().describe("auto: can be automated; manual (default): stays with a person"),
|
|
6826
|
+
technique: import_zod4.z.string().min(1).max(32).optional(),
|
|
6827
|
+
priority: import_zod4.z.string().min(1).optional(),
|
|
6828
|
+
source: import_zod4.z.strictObject({ kind: import_zod4.z.enum(SOURCE_REF_KINDS), value: import_zod4.z.string().min(1).max(100) }).optional().describe("What you read to design it"),
|
|
6829
|
+
rationale: import_zod4.z.string().min(1).max(400).optional().describe("Why the case is worth having")
|
|
6830
|
+
});
|
|
6831
|
+
API_METHODS = ["GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"];
|
|
6832
|
+
proposedEndpoint = import_zod4.z.strictObject({
|
|
6833
|
+
method: import_zod4.z.enum(API_METHODS),
|
|
6834
|
+
path: import_zod4.z.string().min(1).max(512).regex(/^\/[^?#\s]*$/, "a path template starts with / and has no query").describe("A template: /users/{id}, never /users/42"),
|
|
6835
|
+
source: import_zod4.z.enum(["spec", "code", "traffic"]),
|
|
6836
|
+
trust: import_zod4.z.enum(["confirmed", "derived"]).optional(),
|
|
6837
|
+
summary: import_zod4.z.string().trim().min(1).max(500).optional(),
|
|
6838
|
+
auth: import_zod4.z.array(import_zod4.z.string().trim().min(1).max(100)).max(10).optional(),
|
|
6839
|
+
environment: import_zod4.z.string().trim().min(1).max(100).optional(),
|
|
6840
|
+
seenAt: import_zod4.z.string().optional().describe("ISO 8601 date and time with an offset")
|
|
6841
|
+
});
|
|
6842
|
+
REVIEW_STATES = ["pending", "changes_requested", "approved", "rejected"];
|
|
6843
|
+
REVIEW_KINDS = ["result", "proposal", "discovered", "endpoint", "strategy", "report", "memory_rule"];
|
|
6844
|
+
REJECTION_CATEGORIES = ["duplicate", "incorrect", "out-of-scope", "low-value", "not-testable", "other"];
|
|
6845
|
+
entity = import_zod4.z.looseObject({ id: import_zod4.z.string().optional() });
|
|
6846
|
+
plannedRequest = import_zod4.z.object({
|
|
6847
|
+
method: import_zod4.z.string(),
|
|
6848
|
+
path: import_zod4.z.string(),
|
|
6849
|
+
query: import_zod4.z.record(import_zod4.z.string(), import_zod4.z.string()).optional(),
|
|
6850
|
+
body: import_zod4.z.unknown().optional()
|
|
6851
|
+
});
|
|
6852
|
+
writeOutput = import_zod4.z.object({
|
|
6853
|
+
dryRun: import_zod4.z.boolean(),
|
|
6854
|
+
request: plannedRequest.optional(),
|
|
6855
|
+
result: entity.optional()
|
|
6856
|
+
});
|
|
6857
|
+
batchOutput = import_zod4.z.object({
|
|
6858
|
+
dryRun: import_zod4.z.boolean(),
|
|
6859
|
+
requested: import_zod4.z.number().int(),
|
|
6860
|
+
applied: import_zod4.z.number().int(),
|
|
6861
|
+
requests: import_zod4.z.array(plannedRequest).optional(),
|
|
6862
|
+
results: import_zod4.z.array(
|
|
6863
|
+
import_zod4.z.object({
|
|
6864
|
+
ok: import_zod4.z.boolean(),
|
|
6865
|
+
status: import_zod4.z.number().int().optional(),
|
|
6866
|
+
id: import_zod4.z.string().optional(),
|
|
6867
|
+
error: import_zod4.z.string().optional(),
|
|
6868
|
+
code: import_zod4.z.string().optional()
|
|
6869
|
+
})
|
|
6870
|
+
).optional(),
|
|
6871
|
+
report: import_zod4.z.looseObject({}).optional()
|
|
6872
|
+
});
|
|
6873
|
+
bulkPreview = import_zod4.z.looseObject({
|
|
6874
|
+
total: import_zod4.z.number().int(),
|
|
6875
|
+
confirm: import_zod4.z.string(),
|
|
6876
|
+
expiresInSeconds: import_zod4.z.number().int()
|
|
6877
|
+
});
|
|
6878
|
+
}
|
|
6879
|
+
});
|
|
6880
|
+
|
|
6881
|
+
// src/mcp-tools/contract.ts
|
|
6882
|
+
function pluneTool(name) {
|
|
6883
|
+
const tool = BY_NAME.get(name);
|
|
6884
|
+
if (tool === void 0) throw new Error(`unknown tool: ${name}`);
|
|
6885
|
+
return tool;
|
|
6886
|
+
}
|
|
6887
|
+
function isPluneToolName(name) {
|
|
6888
|
+
return BY_NAME.has(name);
|
|
6889
|
+
}
|
|
6890
|
+
function defaultVisibleTools(scope) {
|
|
6891
|
+
return PLUNE_TOOLS.filter((t) => t.visibility === "core" && !(scope === "read" && t.write)).map((t) => t.name);
|
|
6892
|
+
}
|
|
6893
|
+
var import_zod5, MAX_VISIBLE_TOOLS, define, listPage, missionId, PLUNE_TOOLS, PLUNE_TOOL_NAMES, BY_NAME;
|
|
6894
|
+
var init_contract = __esm({
|
|
6895
|
+
"src/mcp-tools/contract.ts"() {
|
|
6896
|
+
"use strict";
|
|
6897
|
+
init_cjs_shims();
|
|
6898
|
+
import_zod5 = require("zod");
|
|
6899
|
+
init_schemas();
|
|
6900
|
+
MAX_VISIBLE_TOOLS = 20;
|
|
6901
|
+
define = (tool) => tool;
|
|
6902
|
+
listPage = {
|
|
6903
|
+
limit: import_zod5.z.number().int().min(1).max(BATCH_MAX2).optional(),
|
|
6904
|
+
cursor: import_zod5.z.string().min(1).optional().describe("nextCursor of the previous page")
|
|
6905
|
+
};
|
|
6906
|
+
missionId = id("The mission id");
|
|
6907
|
+
PLUNE_TOOLS = [
|
|
6908
|
+
define({
|
|
6909
|
+
name: "plune_project_context",
|
|
6910
|
+
title: "Project context",
|
|
6911
|
+
description: "Start here: learn the project you are working in with one call \u2014 its projects, the suite tree, how many test cases it has, its testing missions, and how much waits in the review queue.",
|
|
6912
|
+
input: import_zod5.z.strictObject({}),
|
|
6913
|
+
output: import_zod5.z.object({
|
|
6914
|
+
projectId: import_zod5.z.string().nullable(),
|
|
6915
|
+
projects: import_zod5.z.array(entity),
|
|
6916
|
+
suites: import_zod5.z.array(entity),
|
|
6917
|
+
cases: import_zod5.z.object({ total: import_zod5.z.number().int() }),
|
|
6918
|
+
missions: import_zod5.z.array(entity),
|
|
6919
|
+
reviewQueue: import_zod5.z.object({ pending: import_zod5.z.number().int(), more: import_zod5.z.boolean() })
|
|
6920
|
+
}),
|
|
6921
|
+
write: false,
|
|
6922
|
+
visibility: "core",
|
|
6923
|
+
endpoints: ["GET /v1/projects", "GET /v1/suites", "GET /v1/test-cases", "GET /v1/missions", "GET /v1/review-items"]
|
|
6924
|
+
}),
|
|
6925
|
+
define({
|
|
6926
|
+
name: "plune_get",
|
|
6927
|
+
title: "Read one object",
|
|
6928
|
+
description: "Read one test case, run or review queue entry in full when you already hold its id \u2014 steps, state, history fields and everything the platform keeps on it.",
|
|
6929
|
+
input: import_zod5.z.strictObject({ kind: import_zod5.z.enum(["test-case", "run", "review-item"]), id: id("Its id") }),
|
|
6930
|
+
output: import_zod5.z.object({ kind: import_zod5.z.string(), item: entity }),
|
|
6931
|
+
write: false,
|
|
6932
|
+
visibility: "core",
|
|
6933
|
+
endpoints: ["GET /v1/test-cases/:id", "GET /v1/runs/:id", "GET /v1/review-items/:id"]
|
|
6934
|
+
}),
|
|
6935
|
+
define({
|
|
6936
|
+
name: "plune_cases_search",
|
|
6937
|
+
title: "Find test cases",
|
|
6938
|
+
description: "Find test cases of the project by filter \u2014 suite, state, tag, priority, text and the other filters of the case list \u2014 one page at a time, with the total and how many match.",
|
|
6939
|
+
input: import_zod5.z.strictObject({
|
|
6940
|
+
filters: import_zod5.z.record(import_zod5.z.string(), import_zod5.z.union([import_zod5.z.string(), import_zod5.z.array(import_zod5.z.string())])).optional().describe('Query filters of GET /v1/test-cases: a list is OR inside one key, keys are AND-ed (e.g. {"suiteId": "s1", "tag": ["smoke"]})'),
|
|
6941
|
+
...listPage
|
|
6942
|
+
}),
|
|
6943
|
+
output: import_zod5.z.object({
|
|
6944
|
+
testCases: import_zod5.z.array(entity),
|
|
6945
|
+
total: import_zod5.z.number().int().optional(),
|
|
6946
|
+
matching: import_zod5.z.number().int().optional(),
|
|
6947
|
+
nextCursor: import_zod5.z.string().nullable()
|
|
6948
|
+
}),
|
|
6949
|
+
write: false,
|
|
6950
|
+
visibility: "core",
|
|
6951
|
+
endpoints: ["GET /v1/test-cases?filters"]
|
|
6952
|
+
}),
|
|
6953
|
+
define({
|
|
6954
|
+
name: "plune_review_list",
|
|
6955
|
+
title: "Read the review queue",
|
|
6956
|
+
description: "See what waits for a decision in the review queue \u2014 proposed cases, discovered tests, endpoints, strategies, reports \u2014 narrowed by state and kind, newest first, one page at a time.",
|
|
6957
|
+
input: import_zod5.z.strictObject({
|
|
6958
|
+
state: import_zod5.z.enum(REVIEW_STATES).optional(),
|
|
6959
|
+
kinds: import_zod5.z.array(import_zod5.z.enum(REVIEW_KINDS)).min(1).optional().describe("Questions are read with the question tool"),
|
|
6960
|
+
...listPage
|
|
6961
|
+
}),
|
|
6962
|
+
output: import_zod5.z.object({ items: import_zod5.z.array(entity), nextCursor: import_zod5.z.string().nullable() }),
|
|
6963
|
+
write: false,
|
|
6964
|
+
visibility: "core",
|
|
6965
|
+
endpoints: ["GET /v1/review-items"]
|
|
6966
|
+
}),
|
|
6967
|
+
define({
|
|
6968
|
+
name: "plune_review_decide",
|
|
6969
|
+
title: "Decide queue entries",
|
|
6970
|
+
description: "Approve or reject up to 100 review queue entries in one decision. The first call changes nothing and returns a preview with a confirm value; repeat the same call with confirm to apply it.",
|
|
6971
|
+
input: import_zod5.z.strictObject({
|
|
6972
|
+
itemIds: import_zod5.z.array(import_zod5.z.string().min(1)).min(1).max(BATCH_MAX2).refine((a) => new Set(a).size === a.length, { message: "an id is named once" }),
|
|
6973
|
+
verdict: import_zod5.z.enum(["approved", "rejected"]),
|
|
6974
|
+
rejectionCategory: import_zod5.z.enum(REJECTION_CATEGORIES).optional().describe("Required to reject"),
|
|
6975
|
+
note: import_zod5.z.string().trim().min(1).max(2e3).optional().describe("Why \u2014 required to reject"),
|
|
6976
|
+
confirm: import_zod5.z.string().min(1).optional().describe("The confirm value of the preview")
|
|
6977
|
+
}),
|
|
6978
|
+
output: import_zod5.z.object({
|
|
6979
|
+
requested: import_zod5.z.number().int(),
|
|
6980
|
+
applied: import_zod5.z.number().int(),
|
|
6981
|
+
preview: bulkPreview.optional(),
|
|
6982
|
+
report: import_zod5.z.looseObject({}).optional()
|
|
6983
|
+
}),
|
|
6984
|
+
write: true,
|
|
6985
|
+
visibility: "core",
|
|
6986
|
+
endpoints: ["POST /v1/review-items/decisions"]
|
|
6987
|
+
}),
|
|
6988
|
+
define({
|
|
6989
|
+
name: "plune_case_propose",
|
|
6990
|
+
title: "Propose test cases",
|
|
6991
|
+
description: "Propose up to 100 new test cases for a person to review: they land in the review queue as proposals and become cases only when someone approves them. Nothing is created silently.",
|
|
6992
|
+
input: import_zod5.z.strictObject({
|
|
6993
|
+
target: import_zod5.z.string().trim().min(1).max(2048).describe("What the cases test: the product address, or the mission name"),
|
|
6994
|
+
cases: import_zod5.z.array(proposedCase).min(1).max(BATCH_MAX2),
|
|
6995
|
+
dry_run: dryRun
|
|
6996
|
+
}),
|
|
6997
|
+
output: batchOutput,
|
|
6998
|
+
write: true,
|
|
6999
|
+
visibility: "core",
|
|
7000
|
+
endpoints: ["POST /v1/ingest/cairn"]
|
|
7001
|
+
}),
|
|
7002
|
+
define({
|
|
7003
|
+
name: "plune_mission_create",
|
|
7004
|
+
title: "Create a mission",
|
|
7005
|
+
description: "Create a testing mission for a product: its name, a short description of what the product is, where it runs, and whether documentation exists. It starts as a draft you fill in.",
|
|
7006
|
+
input: import_zod5.z.strictObject({ ...missionFields, dry_run: dryRun }),
|
|
7007
|
+
output: writeOutput,
|
|
7008
|
+
write: true,
|
|
7009
|
+
visibility: "core",
|
|
7010
|
+
endpoints: ["POST /v1/missions"]
|
|
7011
|
+
}),
|
|
7012
|
+
define({
|
|
7013
|
+
name: "plune_mission_get",
|
|
7014
|
+
title: "Read a mission",
|
|
7015
|
+
description: "Read a testing mission with everything it holds \u2014 its brief, state, documentation answer and every source attached to it \u2014 before planning or continuing the work on it.",
|
|
7016
|
+
input: import_zod5.z.strictObject({ missionId }),
|
|
7017
|
+
output: import_zod5.z.object({ mission: entity }),
|
|
7018
|
+
write: false,
|
|
7019
|
+
visibility: "core",
|
|
7020
|
+
endpoints: ["GET /v1/missions/:id"]
|
|
7021
|
+
}),
|
|
7022
|
+
define({
|
|
7023
|
+
name: "plune_mission_update",
|
|
7024
|
+
title: "Update a mission",
|
|
7025
|
+
description: "Change a testing mission: rename it, rewrite the brief, set or remove the address, answer whether documentation exists, or move it between draft, active and done.",
|
|
7026
|
+
input: import_zod5.z.strictObject({
|
|
7027
|
+
missionId,
|
|
7028
|
+
name: missionFields.name.optional(),
|
|
7029
|
+
summary: missionFields.summary,
|
|
7030
|
+
url: missionFields.url,
|
|
7031
|
+
state: missionFields.state,
|
|
7032
|
+
documentation: missionFields.documentation,
|
|
7033
|
+
dry_run: dryRun
|
|
7034
|
+
}).refine((b) => ["name", "summary", "url", "state", "documentation"].some((k) => b[k] !== void 0), {
|
|
7035
|
+
message: "nothing to change"
|
|
7036
|
+
}),
|
|
7037
|
+
output: writeOutput,
|
|
7038
|
+
write: true,
|
|
7039
|
+
visibility: "core",
|
|
7040
|
+
endpoints: ["PATCH /v1/missions/:id"]
|
|
7041
|
+
}),
|
|
7042
|
+
define({
|
|
7043
|
+
name: "plune_source_add",
|
|
7044
|
+
title: "Add a source to a mission",
|
|
7045
|
+
description: "Attach something to read to a mission: a link (page, OpenAPI document, repository, ticket) or the text of a file of the project \u2014 Markdown, plain text or JSON, up to 1 MiB.",
|
|
7046
|
+
input: import_zod5.z.strictObject({
|
|
7047
|
+
missionId,
|
|
7048
|
+
kind: import_zod5.z.enum([...MISSION_LINK_KINDS, "file"]),
|
|
7049
|
+
ref: import_zod5.z.string().optional().describe("The link, for url, openapi, repo and ticket"),
|
|
7050
|
+
name: import_zod5.z.string().optional().describe("The file name, for file (.md, .markdown, .txt, .json)"),
|
|
7051
|
+
text: import_zod5.z.string().optional().describe("The file text, for file"),
|
|
7052
|
+
title: import_zod5.z.string().optional(),
|
|
7053
|
+
version: import_zod5.z.string().optional(),
|
|
7054
|
+
dry_run: dryRun
|
|
7055
|
+
}).superRefine((b, ctx) => {
|
|
7056
|
+
if (b.kind === "file" && (b.name === void 0 || b.text === void 0))
|
|
7057
|
+
ctx.addIssue({ code: "custom", message: "a file source carries name and text" });
|
|
7058
|
+
if (b.kind !== "file" && b.ref === void 0) ctx.addIssue({ code: "custom", message: "a link source carries ref" });
|
|
7059
|
+
}),
|
|
7060
|
+
output: writeOutput,
|
|
7061
|
+
write: true,
|
|
7062
|
+
visibility: "core",
|
|
7063
|
+
endpoints: ["POST /v1/missions/:id/sources", "POST /v1/missions/:id/files"]
|
|
7064
|
+
}),
|
|
7065
|
+
define({
|
|
7066
|
+
name: "plune_question_ask",
|
|
7067
|
+
title: "Ask a person",
|
|
7068
|
+
description: "Ask a person what you do not know instead of guessing: a plain question with up to three options, or a request for access to an API description, the code, staging or a HAR file.",
|
|
7069
|
+
input: questionInput.superRefine((b, ctx) => {
|
|
7070
|
+
if (b.template === "plain" && b.access !== void 0) ctx.addIssue({ code: "custom", message: "access belongs to an access question" });
|
|
7071
|
+
if (b.template === "access" && b.options !== void 0) ctx.addIssue({ code: "custom", message: "options belong to a plain question" });
|
|
7072
|
+
if (b.template === "access" && b.access === void 0) ctx.addIssue({ code: "custom", message: "an access question names the access" });
|
|
7073
|
+
}),
|
|
7074
|
+
output: writeOutput,
|
|
7075
|
+
write: true,
|
|
7076
|
+
visibility: "core",
|
|
7077
|
+
endpoints: ["POST /v1/review-items/questions"]
|
|
7078
|
+
}),
|
|
7079
|
+
define({
|
|
7080
|
+
name: "plune_question_list",
|
|
7081
|
+
title: "Read questions and answers",
|
|
7082
|
+
description: "Check the questions asked to people and the answers they gave \u2014 all of them or one mission's, waiting or answered \u2014 so the work continues from what a person actually said.",
|
|
7083
|
+
input: import_zod5.z.strictObject({ missionId: import_zod5.z.string().min(1).optional(), state: import_zod5.z.enum(REVIEW_STATES).optional(), ...listPage }),
|
|
7084
|
+
output: import_zod5.z.object({ questions: import_zod5.z.array(entity), nextCursor: import_zod5.z.string().nullable() }),
|
|
7085
|
+
write: false,
|
|
7086
|
+
visibility: "core",
|
|
7087
|
+
endpoints: ["GET /v1/review-items?kind=question"]
|
|
7088
|
+
}),
|
|
7089
|
+
define({
|
|
7090
|
+
name: "plune_strategy_propose",
|
|
7091
|
+
title: "Propose a test strategy",
|
|
7092
|
+
description: "Propose a test strategy for a mission in ISTQB sections \u2014 objectives, scope in and out, approach, levels and techniques, risks, entry and exit criteria \u2014 for a person to accept.",
|
|
7093
|
+
input: import_zod5.z.strictObject({ missionId, content: strategyContent, dry_run: dryRun }),
|
|
7094
|
+
output: writeOutput,
|
|
7095
|
+
write: true,
|
|
7096
|
+
visibility: "core",
|
|
7097
|
+
endpoints: ["POST /v1/missions/:id/strategy"]
|
|
7098
|
+
}),
|
|
7099
|
+
define({
|
|
7100
|
+
name: "plune_report_request",
|
|
7101
|
+
title: "Request the mission report",
|
|
7102
|
+
description: "Ask the platform to build the completion report of a mission from its own data and queue it for a person to approve; it needs an accepted strategy first.",
|
|
7103
|
+
input: import_zod5.z.strictObject({ missionId, dry_run: dryRun }),
|
|
7104
|
+
output: writeOutput,
|
|
7105
|
+
write: true,
|
|
7106
|
+
visibility: "core",
|
|
7107
|
+
endpoints: ["POST /v1/missions/:id/report"]
|
|
7108
|
+
}),
|
|
7109
|
+
define({
|
|
7110
|
+
name: "plune_discover_tools",
|
|
7111
|
+
title: "Find more tools",
|
|
7112
|
+
description: "Look for a capability that is not in your tool list \u2014 editing cases, writing results, starting runs, cataloguing API endpoints, a raw API call \u2014 and switch the ones you need on for this session.",
|
|
7113
|
+
input: import_zod5.z.strictObject({
|
|
7114
|
+
query: import_zod5.z.string().trim().min(1).max(200).optional().describe('Words of the task, e.g. "write results"'),
|
|
7115
|
+
names: import_zod5.z.array(import_zod5.z.string().min(1)).min(1).max(MAX_VISIBLE_TOOLS).optional().describe("Tools to switch on")
|
|
7116
|
+
}),
|
|
7117
|
+
output: import_zod5.z.object({
|
|
7118
|
+
revealed: import_zod5.z.array(import_zod5.z.string()),
|
|
7119
|
+
available: import_zod5.z.array(import_zod5.z.object({ name: import_zod5.z.string(), title: import_zod5.z.string(), description: import_zod5.z.string(), write: import_zod5.z.boolean() })),
|
|
7120
|
+
visible: import_zod5.z.array(import_zod5.z.string())
|
|
7121
|
+
}),
|
|
7122
|
+
write: false,
|
|
7123
|
+
visibility: "core",
|
|
7124
|
+
endpoints: ["LOCAL /discover"]
|
|
7125
|
+
}),
|
|
7126
|
+
define({
|
|
7127
|
+
name: "plune_cases_update",
|
|
7128
|
+
title: "Edit test cases",
|
|
7129
|
+
description: "Edit up to 100 existing test cases in one call \u2014 title, steps, expected result, priority, tags, suite \u2014 each applied on its own, with a count of how many were applied.",
|
|
7130
|
+
input: import_zod5.z.strictObject({
|
|
7131
|
+
cases: import_zod5.z.array(import_zod5.z.strictObject({ id: id("The case id"), changes: import_zod5.z.record(import_zod5.z.string(), import_zod5.z.unknown()).describe("Fields of PATCH /v1/test-cases/:id") })).min(1).max(BATCH_MAX2),
|
|
7132
|
+
dry_run: dryRun
|
|
7133
|
+
}),
|
|
7134
|
+
output: batchOutput,
|
|
7135
|
+
write: true,
|
|
7136
|
+
visibility: "discoverable",
|
|
7137
|
+
endpoints: ["PATCH /v1/test-cases/:id"]
|
|
7138
|
+
}),
|
|
7139
|
+
define({
|
|
7140
|
+
name: "plune_run_create",
|
|
7141
|
+
title: "Start a run",
|
|
7142
|
+
description: "Start a test run on the platform to record results into \u2014 named, with its environment and the cases it covers \u2014 using the body of POST /v1/runs (schemaVersion 2).",
|
|
7143
|
+
input: import_zod5.z.strictObject({ run: import_zod5.z.record(import_zod5.z.string(), import_zod5.z.unknown()).describe("The body of POST /v1/runs"), dry_run: dryRun }),
|
|
7144
|
+
output: writeOutput,
|
|
7145
|
+
write: true,
|
|
7146
|
+
visibility: "discoverable",
|
|
7147
|
+
endpoints: ["POST /v1/runs"]
|
|
7148
|
+
}),
|
|
7149
|
+
define({
|
|
7150
|
+
name: "plune_results_write",
|
|
7151
|
+
title: "Write results into a run",
|
|
7152
|
+
description: "Record up to 100 test results into an open run in one request \u2014 passed, failed, skipped, with their keys and evidence \u2014 and learn how many the platform accepted.",
|
|
7153
|
+
input: import_zod5.z.strictObject({
|
|
7154
|
+
runId: id("The run id"),
|
|
7155
|
+
results: import_zod5.z.array(import_zod5.z.record(import_zod5.z.string(), import_zod5.z.unknown())).min(1).max(BATCH_MAX2).describe("Results as POST /v1/runs/:id/results takes them"),
|
|
7156
|
+
combination: import_zod5.z.number().int().min(0).optional(),
|
|
7157
|
+
configuration: import_zod5.z.record(import_zod5.z.string(), import_zod5.z.string()).optional(),
|
|
7158
|
+
dry_run: dryRun
|
|
7159
|
+
}),
|
|
7160
|
+
output: batchOutput,
|
|
7161
|
+
write: true,
|
|
7162
|
+
visibility: "discoverable",
|
|
7163
|
+
endpoints: ["POST /v1/runs/:id/results"]
|
|
7164
|
+
}),
|
|
7165
|
+
define({
|
|
7166
|
+
name: "plune_endpoint_propose",
|
|
7167
|
+
title: "Propose API endpoints",
|
|
7168
|
+
description: "Propose up to 100 API endpoints you found in a spec, the code or traffic for the project's API catalog; each waits in the review queue until a person approves it.",
|
|
7169
|
+
input: import_zod5.z.strictObject({ endpoints: import_zod5.z.array(proposedEndpoint).min(1).max(BATCH_MAX2), dry_run: dryRun }),
|
|
7170
|
+
output: batchOutput,
|
|
7171
|
+
write: true,
|
|
7172
|
+
visibility: "discoverable",
|
|
7173
|
+
endpoints: ["POST /v1/review-items/endpoints"]
|
|
7174
|
+
}),
|
|
7175
|
+
define({
|
|
7176
|
+
name: "plune_api_request",
|
|
7177
|
+
title: "Call the REST API directly",
|
|
7178
|
+
description: "Reach any Plune REST endpoint under /v1 that no other tool covers. Every guard still holds: a deletion is previewed first and needs confirm, and the platform refuses an agent what it refuses over HTTP.",
|
|
7179
|
+
input: import_zod5.z.strictObject({
|
|
7180
|
+
method: import_zod5.z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
|
|
7181
|
+
path: import_zod5.z.string().min(1).max(2048).describe("A path under /v1, e.g. /v1/suites"),
|
|
7182
|
+
query: import_zod5.z.record(import_zod5.z.string(), import_zod5.z.string()).optional(),
|
|
7183
|
+
body: import_zod5.z.unknown().optional(),
|
|
7184
|
+
confirm: import_zod5.z.string().min(1).optional().describe("The confirm value of a preview"),
|
|
7185
|
+
dry_run: dryRun
|
|
7186
|
+
}),
|
|
7187
|
+
output: import_zod5.z.object({
|
|
7188
|
+
status: import_zod5.z.number().int().optional(),
|
|
7189
|
+
body: import_zod5.z.unknown().optional(),
|
|
7190
|
+
dryRun: import_zod5.z.boolean().optional(),
|
|
7191
|
+
request: plannedRequest.optional(),
|
|
7192
|
+
preview: import_zod5.z.looseObject({ method: import_zod5.z.string(), path: import_zod5.z.string(), confirm: import_zod5.z.string(), expiresInSeconds: import_zod5.z.number().int() }).optional()
|
|
7193
|
+
}),
|
|
7194
|
+
write: true,
|
|
7195
|
+
visibility: "discoverable",
|
|
7196
|
+
endpoints: ["ANY /v1/*"]
|
|
7197
|
+
})
|
|
7198
|
+
];
|
|
7199
|
+
PLUNE_TOOL_NAMES = PLUNE_TOOLS.map((t) => t.name);
|
|
7200
|
+
BY_NAME = new Map(PLUNE_TOOLS.map((t) => [t.name, t]));
|
|
7201
|
+
}
|
|
7202
|
+
});
|
|
7203
|
+
|
|
7204
|
+
// src/mcp-tools/client.ts
|
|
7205
|
+
function bodyField(body, key) {
|
|
7206
|
+
if (body === null || typeof body !== "object") return void 0;
|
|
7207
|
+
const value2 = body[key];
|
|
7208
|
+
return typeof value2 === "string" ? value2 : void 0;
|
|
7209
|
+
}
|
|
7210
|
+
function apiBase(apiUrl) {
|
|
7211
|
+
return apiUrl.replace(/\/+$/, "");
|
|
7212
|
+
}
|
|
7213
|
+
function buildUrl(apiUrl, req) {
|
|
7214
|
+
const url = new URL(apiBase(apiUrl) + req.path);
|
|
7215
|
+
for (const [key, value2] of Object.entries(req.query ?? {})) {
|
|
7216
|
+
for (const v of typeof value2 === "string" ? [value2] : value2) url.searchParams.append(key, v);
|
|
7217
|
+
}
|
|
7218
|
+
return url;
|
|
7219
|
+
}
|
|
7220
|
+
function createPluneApi(opts) {
|
|
7221
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
7222
|
+
const headers = {
|
|
7223
|
+
authorization: `Bearer ${opts.token}`,
|
|
7224
|
+
accept: "application/json",
|
|
7225
|
+
"x-plune-session": opts.sessionId,
|
|
7226
|
+
...opts.projectId === void 0 ? {} : { "x-plune-project": opts.projectId },
|
|
7227
|
+
...opts.userAgent === void 0 ? {} : { "user-agent": opts.userAgent }
|
|
7228
|
+
};
|
|
7229
|
+
return async function request(req) {
|
|
7230
|
+
const url = buildUrl(opts.apiUrl, req);
|
|
7231
|
+
let res;
|
|
7232
|
+
try {
|
|
7233
|
+
res = await doFetch(url.toString(), {
|
|
7234
|
+
method: req.method,
|
|
7235
|
+
headers: req.body === void 0 ? headers : { ...headers, "content-type": "application/json" },
|
|
7236
|
+
...req.body === void 0 ? {} : { body: JSON.stringify(req.body) },
|
|
7237
|
+
// Host pinning (#1283): a redirect elsewhere would carry the token there.
|
|
7238
|
+
redirect: "manual"
|
|
7239
|
+
});
|
|
7240
|
+
} catch {
|
|
7241
|
+
throw new PluneNetworkError(apiBase(opts.apiUrl));
|
|
7242
|
+
}
|
|
7243
|
+
const text2 = await res.text();
|
|
7244
|
+
let body = text2;
|
|
7245
|
+
if (text2 !== "") {
|
|
7246
|
+
try {
|
|
7247
|
+
body = JSON.parse(text2);
|
|
7248
|
+
} catch {
|
|
7249
|
+
}
|
|
7250
|
+
} else {
|
|
7251
|
+
body = void 0;
|
|
7252
|
+
}
|
|
7253
|
+
if (res.status < 200 || res.status >= 300) throw new PluneApiError(res.status, body);
|
|
7254
|
+
return { status: res.status, body };
|
|
7255
|
+
};
|
|
7256
|
+
}
|
|
7257
|
+
var PluneApiError, PluneNetworkError;
|
|
7258
|
+
var init_client2 = __esm({
|
|
7259
|
+
"src/mcp-tools/client.ts"() {
|
|
7260
|
+
"use strict";
|
|
7261
|
+
init_cjs_shims();
|
|
7262
|
+
PluneApiError = class extends Error {
|
|
7263
|
+
constructor(status, body) {
|
|
7264
|
+
const error = bodyField(body, "error");
|
|
7265
|
+
super(error ?? `HTTP ${status}`);
|
|
7266
|
+
this.status = status;
|
|
7267
|
+
this.body = body;
|
|
7268
|
+
this.name = "PluneApiError";
|
|
7269
|
+
}
|
|
7270
|
+
status;
|
|
7271
|
+
body;
|
|
7272
|
+
get code() {
|
|
7273
|
+
return bodyField(this.body, "code");
|
|
7274
|
+
}
|
|
7275
|
+
};
|
|
7276
|
+
PluneNetworkError = class extends Error {
|
|
7277
|
+
constructor(apiUrl) {
|
|
7278
|
+
super(`could not reach Plune at ${apiUrl}`);
|
|
7279
|
+
this.apiUrl = apiUrl;
|
|
7280
|
+
this.name = "PluneNetworkError";
|
|
7281
|
+
}
|
|
7282
|
+
apiUrl;
|
|
7283
|
+
};
|
|
7284
|
+
}
|
|
7285
|
+
});
|
|
7286
|
+
|
|
7287
|
+
// src/mcp-tools/execute.ts
|
|
7288
|
+
function errorOf(err) {
|
|
7289
|
+
if (err instanceof PluneApiError) {
|
|
7290
|
+
if (err.status === 401)
|
|
7291
|
+
return { status: 401, code: "token_rejected", message: `Plune rejected the token (401): ${err.message}. Run \`plune login\` or set PLUNE_TOKEN to a live token.` };
|
|
7292
|
+
if (err.status === 403 && err.message === READ_ONLY_REFUSAL)
|
|
7293
|
+
return { status: 403, code: "read_only_token", message: `Plune refused (403): ${readScopeMessage("it cannot write")}` };
|
|
7294
|
+
const code = err.code ?? `http_${err.status}`;
|
|
7295
|
+
return { status: err.status, code, message: `Plune refused (${err.status} ${code}): ${err.message}`, details: err.body };
|
|
7296
|
+
}
|
|
7297
|
+
if (err instanceof PluneNetworkError) return { code: "network", message: `${err.message}. Check PLUNE_API_URL and the network.` };
|
|
7298
|
+
throw err;
|
|
7299
|
+
}
|
|
7300
|
+
function isDeletion(req) {
|
|
7301
|
+
if (req.method === "DELETE") return true;
|
|
7302
|
+
if (asRecord(asRecord(req.body)["action"])["kind"] === "delete") return true;
|
|
7303
|
+
return /\/(bulk-delete|purge|empty)$/.test(req.path);
|
|
7304
|
+
}
|
|
7305
|
+
function safePath(apiUrl, raw) {
|
|
7306
|
+
if (!/^\/v1\/[^\\\s]*$/.test(raw) || raw.startsWith("//") || raw.includes("?") || raw.includes("#")) return void 0;
|
|
7307
|
+
if (raw.split("/").some((s) => s === "." || s === ".." || /^%2e/i.test(s))) return void 0;
|
|
7308
|
+
const base = new URL(apiUrl);
|
|
7309
|
+
const url = new URL(raw, base);
|
|
7310
|
+
return url.origin === base.origin && url.pathname.startsWith("/v1/") ? raw : void 0;
|
|
7311
|
+
}
|
|
7312
|
+
function createPluneToolExecutor(opts) {
|
|
7313
|
+
const sessionId = opts.sessionId ?? (0, import_node_crypto5.randomUUID)();
|
|
7314
|
+
const now = opts.now ?? Date.now;
|
|
7315
|
+
const request = createPluneApi({ ...opts, sessionId });
|
|
7316
|
+
let scope = opts.scope;
|
|
7317
|
+
const revealed = /* @__PURE__ */ new Set();
|
|
7318
|
+
const confirms = /* @__PURE__ */ new Map();
|
|
7319
|
+
let proposals = 0;
|
|
7320
|
+
const visibleTools = () => {
|
|
7321
|
+
const core = defaultVisibleTools(scope);
|
|
7322
|
+
const extra = [...revealed].filter((n) => !(scope === "read" && pluneTool(n).write) && !core.includes(n));
|
|
7323
|
+
return [...core, ...extra];
|
|
7324
|
+
};
|
|
7325
|
+
async function call(req) {
|
|
7326
|
+
try {
|
|
7327
|
+
return await request(req);
|
|
7328
|
+
} catch (err) {
|
|
7329
|
+
if (err instanceof PluneApiError && err.status === 403 && err.message === READ_ONLY_REFUSAL) scope = "read";
|
|
7330
|
+
throw err;
|
|
7331
|
+
}
|
|
7332
|
+
}
|
|
7333
|
+
const planned = (req) => withoutUndefined({ method: req.method, path: req.path, query: req.query, body: req.body });
|
|
7334
|
+
async function write(req, dry) {
|
|
7335
|
+
if (dry === true) return { dryRun: true, request: planned(req) };
|
|
7336
|
+
const res = await call(req);
|
|
7337
|
+
return { dryRun: false, result: asRecord(res.body) };
|
|
7338
|
+
}
|
|
7339
|
+
async function batch(reqs, dry, idOf2) {
|
|
7340
|
+
if (dry === true) return { dryRun: true, requested: reqs.length, applied: 0, requests: reqs.map(planned) };
|
|
7341
|
+
const results = [];
|
|
7342
|
+
for (const [i, req] of reqs.entries()) {
|
|
7343
|
+
try {
|
|
7344
|
+
const res = await call(req);
|
|
7345
|
+
results.push(withoutUndefined({ ok: true, status: res.status, id: idOf2(res.body, i) }));
|
|
7346
|
+
} catch (err) {
|
|
7347
|
+
const e = errorOf(err);
|
|
7348
|
+
if (e.code === "token_rejected" || e.code === "read_only_token" || e.code === "network") throw err;
|
|
7349
|
+
results.push(withoutUndefined({ ok: false, status: e.status, error: e.message, code: e.code }));
|
|
7350
|
+
}
|
|
7351
|
+
}
|
|
7352
|
+
return { dryRun: false, requested: reqs.length, applied: results.filter((r) => r["ok"] === true).length, results };
|
|
7353
|
+
}
|
|
7354
|
+
const fingerprint = (req) => JSON.stringify([req.method, req.path, req.query ?? {}, req.body ?? null]);
|
|
7355
|
+
const str2 = (v) => v;
|
|
7356
|
+
const handlers = {
|
|
7357
|
+
async plune_project_context() {
|
|
7358
|
+
const [projects, suites, cases, missions, queue] = await Promise.all([
|
|
7359
|
+
call({ method: "GET", path: "/v1/projects" }),
|
|
7360
|
+
call({ method: "GET", path: "/v1/suites" }),
|
|
7361
|
+
call({ method: "GET", path: "/v1/test-cases", query: { limit: "1", stability: "0" } }),
|
|
7362
|
+
call({ method: "GET", path: "/v1/missions" }),
|
|
7363
|
+
call({ method: "GET", path: "/v1/review-items", query: { state: "pending", limit: "100" } })
|
|
7364
|
+
]);
|
|
7365
|
+
const projectList = asArray(asRecord(projects.body)["projects"]);
|
|
7366
|
+
const only = projectList.length === 1 ? asRecord(projectList[0])["id"] : void 0;
|
|
7367
|
+
const projectId = opts.projectId ?? (typeof only === "string" ? only : null);
|
|
7368
|
+
const total = asRecord(cases.body)["total"];
|
|
7369
|
+
return {
|
|
7370
|
+
projectId,
|
|
7371
|
+
projects: projectList,
|
|
7372
|
+
suites: asArray(asRecord(suites.body)["suites"]),
|
|
7373
|
+
cases: { total: typeof total === "number" ? total : 0 },
|
|
7374
|
+
missions: asArray(asRecord(missions.body)["missions"]),
|
|
7375
|
+
reviewQueue: { pending: asArray(asRecord(queue.body)["items"]).length, more: cursorOf(asRecord(queue.body)["nextCursor"]) !== null }
|
|
7376
|
+
};
|
|
7377
|
+
},
|
|
7378
|
+
async plune_get(i) {
|
|
7379
|
+
const route = { "test-case": "/v1/test-cases/:id", run: "/v1/runs/:id", "review-item": "/v1/review-items/:id" }[str2(i["kind"])];
|
|
7380
|
+
const res = await call({ method: "GET", path: path19(route, str2(i["id"])) });
|
|
7381
|
+
return { kind: str2(i["kind"]), item: asRecord(res.body) };
|
|
7382
|
+
},
|
|
7383
|
+
async plune_cases_search(i) {
|
|
7384
|
+
const query = withoutUndefined({
|
|
7385
|
+
...i["filters"],
|
|
7386
|
+
limit: i["limit"] === void 0 ? void 0 : String(i["limit"]),
|
|
7387
|
+
cursor: i["cursor"],
|
|
7388
|
+
stability: "0"
|
|
7389
|
+
});
|
|
7390
|
+
const body = asRecord((await call({ method: "GET", path: "/v1/test-cases", query })).body);
|
|
7391
|
+
return withoutUndefined({
|
|
7392
|
+
testCases: asArray(body["testCases"]),
|
|
7393
|
+
total: typeof body["total"] === "number" ? body["total"] : void 0,
|
|
7394
|
+
matching: typeof body["matching"] === "number" ? body["matching"] : void 0,
|
|
7395
|
+
nextCursor: cursorOf(body["nextCursor"])
|
|
7396
|
+
});
|
|
7397
|
+
},
|
|
7398
|
+
async plune_review_list(i) {
|
|
7399
|
+
const kinds = i["kinds"];
|
|
7400
|
+
const query = withoutUndefined({
|
|
7401
|
+
state: i["state"],
|
|
7402
|
+
kind: kinds?.join(","),
|
|
7403
|
+
limit: i["limit"] === void 0 ? void 0 : String(i["limit"]),
|
|
7404
|
+
cursor: i["cursor"]
|
|
7405
|
+
});
|
|
7406
|
+
const body = asRecord((await call({ method: "GET", path: "/v1/review-items", query })).body);
|
|
7407
|
+
return { items: asArray(body["items"]), nextCursor: cursorOf(body["nextCursor"]) };
|
|
7408
|
+
},
|
|
7409
|
+
async plune_review_decide(i) {
|
|
7410
|
+
const itemIds = i["itemIds"];
|
|
7411
|
+
const res = await call({
|
|
7412
|
+
method: "POST",
|
|
7413
|
+
path: "/v1/review-items/decisions",
|
|
7414
|
+
...i["confirm"] === void 0 ? {} : { query: { confirm: str2(i["confirm"]) } },
|
|
7415
|
+
body: withoutUndefined({ itemIds, state: i["verdict"], rejectionCategory: i["rejectionCategory"], note: i["note"] })
|
|
7416
|
+
});
|
|
7417
|
+
const body = asRecord(res.body);
|
|
7418
|
+
if (body["preview"] !== void 0) return { requested: itemIds.length, applied: 0, preview: asRecord(body["preview"]) };
|
|
7419
|
+
const applied = body["applied"];
|
|
7420
|
+
return { requested: itemIds.length, applied: typeof applied === "number" ? applied : 0, report: body };
|
|
7421
|
+
},
|
|
7422
|
+
async plune_case_propose(i) {
|
|
7423
|
+
const cases = i["cases"].map((c) => {
|
|
7424
|
+
const steps = c["steps"];
|
|
7425
|
+
const stableId = (0, import_node_crypto5.createHash)("sha256").update([c["title"], ...steps, c["expected"]].join("\n")).digest("hex").slice(0, 32);
|
|
7426
|
+
return withoutUndefined({ stableId, execution: "manual", ...c });
|
|
7427
|
+
});
|
|
7428
|
+
proposals += 1;
|
|
7429
|
+
const req = {
|
|
7430
|
+
method: "POST",
|
|
7431
|
+
path: "/v1/ingest/cairn",
|
|
7432
|
+
body: { schemaVersion: 1, run: { runId: `mcp-${sessionId}-${proposals}`, mode: "design", target: str2(i["target"]) }, cases }
|
|
7433
|
+
};
|
|
7434
|
+
if (i["dry_run"] === true) return { dryRun: true, requested: cases.length, applied: 0, requests: [planned(req)] };
|
|
7435
|
+
const summary = asRecord((await call(req)).body);
|
|
7436
|
+
const proposed = summary["proposed"];
|
|
7437
|
+
return { dryRun: false, requested: cases.length, applied: typeof proposed === "number" ? proposed : 0, report: summary };
|
|
7438
|
+
},
|
|
7439
|
+
async plune_mission_create(i) {
|
|
7440
|
+
const { dry_run, ...body } = i;
|
|
7441
|
+
return write({ method: "POST", path: "/v1/missions", body: withoutUndefined(body) }, dry_run);
|
|
7442
|
+
},
|
|
7443
|
+
async plune_mission_get(i) {
|
|
7444
|
+
const res = await call({ method: "GET", path: path19("/v1/missions/:id", str2(i["missionId"])) });
|
|
7445
|
+
return { mission: asRecord(res.body) };
|
|
7446
|
+
},
|
|
7447
|
+
async plune_mission_update(i) {
|
|
7448
|
+
const { dry_run, missionId: missionId2, ...body } = i;
|
|
7449
|
+
return write({ method: "PATCH", path: path19("/v1/missions/:id", str2(missionId2)), body: withoutUndefined(body) }, dry_run);
|
|
7450
|
+
},
|
|
7451
|
+
async plune_source_add(i) {
|
|
7452
|
+
const parsed = (i["kind"] === "file" ? fileSource : linkSource).safeParse(withoutUndefined(i));
|
|
7453
|
+
if (!parsed.success) return fail("invalid_input", `The input does not fit: ${zodMessage(parsed.error)}`);
|
|
7454
|
+
const { dry_run, missionId: missionId2, kind, ...rest } = parsed.data;
|
|
7455
|
+
const req = kind === "file" ? { method: "POST", path: path19("/v1/missions/:id/files", str2(missionId2)), body: withoutUndefined(rest) } : { method: "POST", path: path19("/v1/missions/:id/sources", str2(missionId2)), body: withoutUndefined({ kind, ...rest }) };
|
|
7456
|
+
return write(req, dry_run);
|
|
7457
|
+
},
|
|
7458
|
+
async plune_question_ask(i) {
|
|
7459
|
+
const { dry_run, ...body } = i;
|
|
7460
|
+
return write({ method: "POST", path: "/v1/review-items/questions", body: withoutUndefined(body) }, dry_run);
|
|
7461
|
+
},
|
|
7462
|
+
async plune_question_list(i) {
|
|
7463
|
+
const query = withoutUndefined({
|
|
7464
|
+
kind: "question",
|
|
7465
|
+
state: i["state"],
|
|
7466
|
+
limit: i["limit"] === void 0 ? void 0 : String(i["limit"]),
|
|
7467
|
+
cursor: i["cursor"]
|
|
7468
|
+
});
|
|
7469
|
+
const body = asRecord((await call({ method: "GET", path: "/v1/review-items", query })).body);
|
|
7470
|
+
const missionId2 = i["missionId"];
|
|
7471
|
+
const questions = asArray(body["items"]).filter(
|
|
7472
|
+
(item) => missionId2 === void 0 || asRecord(asRecord(item)["question"])["missionId"] === missionId2
|
|
7473
|
+
);
|
|
7474
|
+
return { questions, nextCursor: cursorOf(body["nextCursor"]) };
|
|
7475
|
+
},
|
|
7476
|
+
async plune_strategy_propose(i) {
|
|
7477
|
+
return write({ method: "POST", path: path19("/v1/missions/:id/strategy", str2(i["missionId"])), body: i["content"] }, i["dry_run"]);
|
|
7478
|
+
},
|
|
7479
|
+
async plune_report_request(i) {
|
|
7480
|
+
return write({ method: "POST", path: path19("/v1/missions/:id/report", str2(i["missionId"])), body: {} }, i["dry_run"]);
|
|
7481
|
+
},
|
|
7482
|
+
async plune_discover_tools(i) {
|
|
7483
|
+
const names = i["names"] ?? [];
|
|
7484
|
+
const unknown = names.filter((n) => !isPluneToolName(n));
|
|
7485
|
+
if (unknown.length > 0) return fail("invalid_input", `No such tool: ${unknown.join(", ")}. Call this tool without names to see what there is.`);
|
|
7486
|
+
const refused = names.filter((n) => scope === "read" && pluneTool(n).write);
|
|
7487
|
+
if (refused.length > 0) return fail("read_only_token", readScopeMessage(`${refused.join(", ")} cannot be switched on`));
|
|
7488
|
+
for (const n of names) revealed.add(n);
|
|
7489
|
+
const words = (i["query"] ?? "").toLowerCase().split(/\s+/).filter(Boolean);
|
|
7490
|
+
const visible = visibleTools();
|
|
7491
|
+
const available = PLUNE_TOOLS.filter((t) => !visible.includes(t.name) && !(scope === "read" && t.write)).filter((t) => words.every((w) => `${t.name} ${t.title} ${t.description}`.toLowerCase().includes(w))).map((t) => ({ name: t.name, title: t.title, description: t.description, write: t.write }));
|
|
7492
|
+
return { revealed: names, available, visible };
|
|
7493
|
+
},
|
|
7494
|
+
async plune_cases_update(i) {
|
|
7495
|
+
const cases = i["cases"];
|
|
7496
|
+
return batch(
|
|
7497
|
+
cases.map((c) => ({ method: "PATCH", path: path19("/v1/test-cases/:id", c.id), body: c.changes })),
|
|
7498
|
+
i["dry_run"],
|
|
7499
|
+
(_b, n) => cases[n]?.id
|
|
7500
|
+
);
|
|
7501
|
+
},
|
|
7502
|
+
async plune_run_create(i) {
|
|
7503
|
+
return write({ method: "POST", path: "/v1/runs", body: i["run"] }, i["dry_run"]);
|
|
7504
|
+
},
|
|
7505
|
+
async plune_results_write(i) {
|
|
7506
|
+
const results = i["results"];
|
|
7507
|
+
const req = {
|
|
7508
|
+
method: "POST",
|
|
7509
|
+
path: path19("/v1/runs/:id/results", str2(i["runId"])),
|
|
7510
|
+
body: withoutUndefined({ results, combination: i["combination"], configuration: i["configuration"] })
|
|
7511
|
+
};
|
|
7512
|
+
if (i["dry_run"] === true) return { dryRun: true, requested: results.length, applied: 0, requests: [planned(req)] };
|
|
7513
|
+
const report = asRecord((await call(req)).body);
|
|
7514
|
+
const accepted = asRecord(report["counts"])["accepted"];
|
|
7515
|
+
return { dryRun: false, requested: results.length, applied: typeof accepted === "number" ? accepted : 0, report };
|
|
7516
|
+
},
|
|
7517
|
+
async plune_endpoint_propose(i) {
|
|
7518
|
+
return batch(
|
|
7519
|
+
i["endpoints"].map((e) => ({ method: "POST", path: "/v1/review-items/endpoints", body: withoutUndefined(e) })),
|
|
7520
|
+
i["dry_run"],
|
|
7521
|
+
(b) => asRecord(b)["id"]
|
|
7522
|
+
);
|
|
7523
|
+
},
|
|
7524
|
+
async plune_api_request(i) {
|
|
7525
|
+
const method = str2(i["method"]);
|
|
7526
|
+
const p = safePath(opts.apiUrl, str2(i["path"]));
|
|
7527
|
+
if (p === void 0) return fail("invalid_input", `path must be a path under /v1 on ${new URL(opts.apiUrl).origin}, e.g. /v1/suites \u2014 no host, no dot segments, no query (use query)`);
|
|
7528
|
+
const query = i["query"];
|
|
7529
|
+
const req = withoutUndefined({ method, path: p, query, body: i["body"] });
|
|
7530
|
+
if (i["dry_run"] === true) return { dryRun: true, request: planned(req) };
|
|
7531
|
+
const confirm2 = i["confirm"];
|
|
7532
|
+
if (isDeletion(req)) {
|
|
7533
|
+
const t = now();
|
|
7534
|
+
for (const [k, v] of confirms) if (v.expires <= t) confirms.delete(k);
|
|
7535
|
+
if (confirm2 === void 0) {
|
|
7536
|
+
const value2 = (0, import_node_crypto5.randomBytes)(24).toString("base64url");
|
|
7537
|
+
confirms.set(value2, { fingerprint: fingerprint(req), expires: t + CONFIRM_TTL_MS });
|
|
7538
|
+
return {
|
|
7539
|
+
preview: {
|
|
7540
|
+
action: "delete",
|
|
7541
|
+
method,
|
|
7542
|
+
path: p,
|
|
7543
|
+
confirm: value2,
|
|
7544
|
+
expiresInSeconds: CONFIRM_TTL_MS / 1e3,
|
|
7545
|
+
note: "Nothing was sent. Repeat the same call with confirm to send it. The platform refuses an agent any deletion (agent_cannot_delete, bulk_delete_needs_person): a person deletes from the dashboard."
|
|
7546
|
+
}
|
|
7547
|
+
};
|
|
7548
|
+
}
|
|
7549
|
+
const held = confirms.get(confirm2);
|
|
7550
|
+
confirms.delete(confirm2);
|
|
7551
|
+
if (held === void 0 || held.fingerprint !== fingerprint(req))
|
|
7552
|
+
return fail("confirm_invalid", "confirm is unknown, expired, used, or for another request \u2014 repeat the call without it to get a new preview");
|
|
7553
|
+
} else if (confirm2 !== void 0) {
|
|
7554
|
+
req.query = { ...query, confirm: confirm2 };
|
|
7555
|
+
}
|
|
7556
|
+
const res = await call(req);
|
|
7557
|
+
return { status: res.status, body: res.body };
|
|
7558
|
+
}
|
|
7559
|
+
};
|
|
7560
|
+
return {
|
|
7561
|
+
sessionId,
|
|
7562
|
+
scope: () => scope,
|
|
7563
|
+
visibleTools,
|
|
7564
|
+
async execute(name, input) {
|
|
7565
|
+
const handler = isPluneToolName(name) ? handlers[name] : void 0;
|
|
7566
|
+
if (handler === void 0) return fail("unknown_tool", `No such tool: ${name}.`);
|
|
7567
|
+
const tool = pluneTool(name);
|
|
7568
|
+
const parsed = tool.input.safeParse(input ?? {});
|
|
7569
|
+
if (!parsed.success) return fail("invalid_input", `The input does not fit: ${zodMessage(parsed.error)}`);
|
|
7570
|
+
const args = parsed.data;
|
|
7571
|
+
const writes = name === "plune_api_request" ? rawWrites(str2(args["method"]), str2(args["path"])) : tool.write;
|
|
7572
|
+
if (writes && scope === "read" && args["dry_run"] !== true) return fail("read_only_token", readScopeMessage(`${name} is refused before any request`));
|
|
7573
|
+
try {
|
|
7574
|
+
const out = await handler(args);
|
|
7575
|
+
return "ok" in out && typeof out["ok"] === "boolean" && ("error" in out || "data" in out) ? out : { ok: true, data: out };
|
|
7576
|
+
} catch (err) {
|
|
7577
|
+
return { ok: false, error: errorOf(err) };
|
|
7578
|
+
}
|
|
7579
|
+
}
|
|
7580
|
+
};
|
|
7581
|
+
}
|
|
7582
|
+
var import_node_crypto5, CONFIRM_TTL_MS, READ_ONLY_REFUSAL, readScopeMessage, fail, zodMessage, path19, withoutUndefined, asRecord, asArray, cursorOf, rawWrites;
|
|
7583
|
+
var init_execute = __esm({
|
|
7584
|
+
"src/mcp-tools/execute.ts"() {
|
|
7585
|
+
"use strict";
|
|
7586
|
+
init_cjs_shims();
|
|
7587
|
+
import_node_crypto5 = require("crypto");
|
|
7588
|
+
init_client2();
|
|
7589
|
+
init_contract();
|
|
7590
|
+
init_schemas();
|
|
7591
|
+
CONFIRM_TTL_MS = 5 * 60 * 1e3;
|
|
7592
|
+
READ_ONLY_REFUSAL = "read-only token";
|
|
7593
|
+
readScopeMessage = (what) => `This token has the read scope, so ${what}. Use a token with the write scope (Plune \u2192 Account \u2192 API tokens) and restart the server.`;
|
|
7594
|
+
fail = (code, message, extra = {}) => ({
|
|
7595
|
+
ok: false,
|
|
7596
|
+
error: { code, message, ...extra }
|
|
7597
|
+
});
|
|
7598
|
+
zodMessage = (error) => error.issues.map((i) => `${i.path.join(".") || "(input)"}: ${i.message}`).join("; ");
|
|
7599
|
+
path19 = (template, ...ids) => {
|
|
7600
|
+
let i = 0;
|
|
7601
|
+
return template.replace(/:[a-z]+/gi, () => encodeURIComponent(ids[i++] ?? ""));
|
|
7602
|
+
};
|
|
7603
|
+
withoutUndefined = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
|
|
7604
|
+
asRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
7605
|
+
asArray = (v) => Array.isArray(v) ? v : [];
|
|
7606
|
+
cursorOf = (v) => typeof v === "string" ? v : null;
|
|
7607
|
+
rawWrites = (method, p) => method !== "GET" && !(method === "POST" && p === "/v1/test-cases/resolve");
|
|
7608
|
+
}
|
|
7609
|
+
});
|
|
7610
|
+
|
|
7611
|
+
// src/mcp-tools/index.ts
|
|
7612
|
+
var init_mcp_tools = __esm({
|
|
7613
|
+
"src/mcp-tools/index.ts"() {
|
|
7614
|
+
"use strict";
|
|
7615
|
+
init_cjs_shims();
|
|
7616
|
+
init_contract();
|
|
7617
|
+
init_execute();
|
|
7618
|
+
init_client2();
|
|
7619
|
+
init_schemas();
|
|
7620
|
+
}
|
|
7621
|
+
});
|
|
7622
|
+
|
|
7623
|
+
// src/mcp/server.ts
|
|
7624
|
+
var server_exports = {};
|
|
7625
|
+
__export(server_exports, {
|
|
7626
|
+
createPluneMcpServer: () => createPluneMcpServer,
|
|
7627
|
+
serveStdio: () => serveStdio
|
|
7628
|
+
});
|
|
7629
|
+
function toResult(outcome) {
|
|
7630
|
+
if (outcome.ok) {
|
|
7631
|
+
return { content: [{ type: "text", text: JSON.stringify(outcome.data, null, 2) }], structuredContent: outcome.data };
|
|
7632
|
+
}
|
|
7633
|
+
return {
|
|
7634
|
+
isError: true,
|
|
7635
|
+
content: [{ type: "text", text: outcome.error.message }],
|
|
7636
|
+
structuredContent: { error: outcome.error }
|
|
7637
|
+
};
|
|
7638
|
+
}
|
|
7639
|
+
function createPluneMcpServer(opts) {
|
|
7640
|
+
const { version, ...executorOptions } = opts;
|
|
7641
|
+
const executor = createPluneToolExecutor({ ...executorOptions, userAgent: executorOptions.userAgent ?? `plune-cli/${version} mcp` });
|
|
7642
|
+
const server = new import_mcp.McpServer({ name: "plune", version }, { capabilities: { tools: { listChanged: true } } });
|
|
7643
|
+
const registered = /* @__PURE__ */ new Map();
|
|
7644
|
+
const sync = () => {
|
|
7645
|
+
const visible = new Set(executor.visibleTools());
|
|
7646
|
+
for (const [name, tool] of registered) {
|
|
7647
|
+
if (visible.has(name) && !tool.enabled) tool.enable();
|
|
7648
|
+
else if (!visible.has(name) && tool.enabled) tool.disable();
|
|
7649
|
+
}
|
|
7650
|
+
};
|
|
7651
|
+
for (const tool of PLUNE_TOOLS) {
|
|
7652
|
+
if (opts.scope === "read" && tool.write) continue;
|
|
7653
|
+
const handle = server.registerTool(
|
|
7654
|
+
tool.name,
|
|
7655
|
+
{
|
|
7656
|
+
title: tool.title,
|
|
7657
|
+
description: tool.description,
|
|
7658
|
+
inputSchema: tool.input,
|
|
7659
|
+
outputSchema: tool.output,
|
|
7660
|
+
annotations: {
|
|
7661
|
+
title: tool.title,
|
|
7662
|
+
readOnlyHint: !tool.write,
|
|
7663
|
+
destructiveHint: tool.name === "plune_api_request",
|
|
7664
|
+
openWorldHint: false
|
|
7665
|
+
}
|
|
7666
|
+
},
|
|
7667
|
+
async (args) => {
|
|
7668
|
+
const outcome = await executor.execute(tool.name, args);
|
|
7669
|
+
sync();
|
|
7670
|
+
return toResult(outcome);
|
|
7671
|
+
}
|
|
7672
|
+
);
|
|
7673
|
+
registered.set(tool.name, handle);
|
|
7674
|
+
}
|
|
7675
|
+
sync();
|
|
7676
|
+
return server;
|
|
7677
|
+
}
|
|
7678
|
+
async function serveStdio(opts) {
|
|
7679
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
7680
|
+
const server = createPluneMcpServer(opts);
|
|
7681
|
+
const transport = new StdioServerTransport();
|
|
7682
|
+
const closed = new Promise((resolve11) => {
|
|
7683
|
+
transport.onclose = () => resolve11();
|
|
7684
|
+
});
|
|
7685
|
+
await server.connect(transport);
|
|
7686
|
+
await closed;
|
|
7687
|
+
}
|
|
7688
|
+
var import_mcp;
|
|
7689
|
+
var init_server = __esm({
|
|
7690
|
+
"src/mcp/server.ts"() {
|
|
7691
|
+
"use strict";
|
|
7692
|
+
init_cjs_shims();
|
|
7693
|
+
import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
7694
|
+
init_mcp_tools();
|
|
7695
|
+
}
|
|
7696
|
+
});
|
|
7697
|
+
|
|
7698
|
+
// src/cli/commands/mcp.ts
|
|
7699
|
+
var mcp_exports = {};
|
|
7700
|
+
__export(mcp_exports, {
|
|
7701
|
+
handleMcpServe: () => handleMcpServe
|
|
7702
|
+
});
|
|
7703
|
+
async function handleMcpServe(flags, deps) {
|
|
7704
|
+
const token = (deps.loadToken ?? loadToken)();
|
|
7705
|
+
if (token === null) throw new NotLoggedInError();
|
|
7706
|
+
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
7707
|
+
const projectId = flags.project?.trim() || process.env["PLUNE_PROJECT"]?.trim() || void 0;
|
|
7708
|
+
const serve = deps.serve ?? (await Promise.resolve().then(() => (init_server(), server_exports))).serveStdio;
|
|
7709
|
+
await serve({
|
|
7710
|
+
token,
|
|
7711
|
+
apiUrl,
|
|
7712
|
+
version: deps.version,
|
|
7713
|
+
...projectId !== void 0 ? { projectId } : {},
|
|
7714
|
+
...flags.readOnly === true ? { scope: "read" } : {}
|
|
7715
|
+
});
|
|
7716
|
+
}
|
|
7717
|
+
var init_mcp = __esm({
|
|
7718
|
+
"src/cli/commands/mcp.ts"() {
|
|
7719
|
+
"use strict";
|
|
7720
|
+
init_cjs_shims();
|
|
7721
|
+
init_credentials();
|
|
7722
|
+
init_api_url();
|
|
7723
|
+
init_sync();
|
|
7724
|
+
}
|
|
7725
|
+
});
|
|
7726
|
+
|
|
6692
7727
|
// src/cli/commands/cairn-artifact.ts
|
|
6693
7728
|
async function findLatestCairnRun(runsDir = "runs") {
|
|
6694
7729
|
const entries = await fs17.readdir(runsDir, { withFileTypes: true }).catch(() => null);
|
|
@@ -6696,9 +7731,9 @@ async function findLatestCairnRun(runsDir = "runs") {
|
|
|
6696
7731
|
let newest;
|
|
6697
7732
|
for (const entry of entries) {
|
|
6698
7733
|
if (!entry.isDirectory()) continue;
|
|
6699
|
-
const dir =
|
|
7734
|
+
const dir = path20.join(runsDir, entry.name);
|
|
6700
7735
|
try {
|
|
6701
|
-
const report = await fs17.lstat(
|
|
7736
|
+
const report = await fs17.lstat(path20.join(dir, "report.json"));
|
|
6702
7737
|
if (!report.isFile()) continue;
|
|
6703
7738
|
if (newest === void 0 || report.mtimeMs > newest.at) {
|
|
6704
7739
|
newest = { dir, at: report.mtimeMs };
|
|
@@ -6717,10 +7752,10 @@ async function readJson(file) {
|
|
|
6717
7752
|
}
|
|
6718
7753
|
}
|
|
6719
7754
|
async function defaultReadEvidence(dir) {
|
|
6720
|
-
const raw = await readJson(
|
|
7755
|
+
const raw = await readJson(path20.join(dir, "api-evidence.json"));
|
|
6721
7756
|
if (!Array.isArray(raw)) return void 0;
|
|
6722
7757
|
return raw.map((r) => {
|
|
6723
|
-
const rec =
|
|
7758
|
+
const rec = asRecord2(r);
|
|
6724
7759
|
const error = str(rec["error"]);
|
|
6725
7760
|
return { name: str(rec["name"]), passed: rec["passed"] === true, ...error !== "" ? { error } : {} };
|
|
6726
7761
|
});
|
|
@@ -6728,7 +7763,7 @@ async function defaultReadEvidence(dir) {
|
|
|
6728
7763
|
function casesFromDesign(report) {
|
|
6729
7764
|
const list2 = Array.isArray(report["testCases"]) ? report["testCases"] : [];
|
|
6730
7765
|
return list2.map((raw) => {
|
|
6731
|
-
const c =
|
|
7766
|
+
const c = asRecord2(raw);
|
|
6732
7767
|
const steps = (Array.isArray(c["steps"]) ? c["steps"] : []).map((s) => str(s)).filter((s) => s !== "");
|
|
6733
7768
|
return {
|
|
6734
7769
|
stableId: str(c["stableId"]),
|
|
@@ -6744,7 +7779,7 @@ function casesFromDesign(report) {
|
|
|
6744
7779
|
function casesFromApi(report, verdicts) {
|
|
6745
7780
|
const list2 = Array.isArray(report["cases"]) ? report["cases"] : [];
|
|
6746
7781
|
return list2.map((raw) => {
|
|
6747
|
-
const c =
|
|
7782
|
+
const c = asRecord2(raw);
|
|
6748
7783
|
const name = str(c["name"], "unnamed operation");
|
|
6749
7784
|
const request = `${str(c["method"], "GET")} ${str(c["path"], "/")}`;
|
|
6750
7785
|
const verdict = verdicts.get(name);
|
|
@@ -6760,9 +7795,9 @@ function casesFromApi(report, verdicts) {
|
|
|
6760
7795
|
});
|
|
6761
7796
|
}
|
|
6762
7797
|
async function readCairnRun(dir, deps = {}) {
|
|
6763
|
-
const raw = await readJson(
|
|
7798
|
+
const raw = await readJson(path20.join(dir, "report.json"));
|
|
6764
7799
|
if (raw === void 0) throw new CairnRunNotFoundError(dir);
|
|
6765
|
-
const report =
|
|
7800
|
+
const report = asRecord2(raw);
|
|
6766
7801
|
if (report["schemaVersion"] !== SUPPORTED_ARTIFACT_VERSION) {
|
|
6767
7802
|
throw new CairnRunVersionError(report["schemaVersion"]);
|
|
6768
7803
|
}
|
|
@@ -6790,20 +7825,20 @@ async function readCairnRun(dir, deps = {}) {
|
|
|
6790
7825
|
return {
|
|
6791
7826
|
schemaVersion: INGEST_SCHEMA_VERSION,
|
|
6792
7827
|
run: {
|
|
6793
|
-
runId: str(report["runId"],
|
|
7828
|
+
runId: str(report["runId"], path20.basename(dir)),
|
|
6794
7829
|
mode,
|
|
6795
7830
|
target: str(report["url"], "unknown")
|
|
6796
7831
|
},
|
|
6797
7832
|
cases
|
|
6798
7833
|
};
|
|
6799
7834
|
}
|
|
6800
|
-
var fs17,
|
|
7835
|
+
var fs17, path20, SUPPORTED_ARTIFACT_VERSION, INGEST_SCHEMA_VERSION, NoCairnRunsError, CairnRunNotFoundError, CairnRunVersionError, CairnRunPartialError, CairnEvidenceAmbiguousError, asRecord2, str;
|
|
6801
7836
|
var init_cairn_artifact = __esm({
|
|
6802
7837
|
"src/cli/commands/cairn-artifact.ts"() {
|
|
6803
7838
|
"use strict";
|
|
6804
7839
|
init_cjs_shims();
|
|
6805
7840
|
fs17 = __toESM(require("fs/promises"), 1);
|
|
6806
|
-
|
|
7841
|
+
path20 = __toESM(require("path"), 1);
|
|
6807
7842
|
SUPPORTED_ARTIFACT_VERSION = 1;
|
|
6808
7843
|
INGEST_SCHEMA_VERSION = 1;
|
|
6809
7844
|
NoCairnRunsError = class extends Error {
|
|
@@ -6842,7 +7877,7 @@ var init_cairn_artifact = __esm({
|
|
|
6842
7877
|
this.name = "CairnEvidenceAmbiguousError";
|
|
6843
7878
|
}
|
|
6844
7879
|
};
|
|
6845
|
-
|
|
7880
|
+
asRecord2 = (v) => typeof v === "object" && v !== null ? v : {};
|
|
6846
7881
|
str = (v, fallback = "") => typeof v === "string" && v !== "" ? v : fallback;
|
|
6847
7882
|
}
|
|
6848
7883
|
});
|
|
@@ -6875,7 +7910,7 @@ async function handleIngest(deps) {
|
|
|
6875
7910
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
6876
7911
|
const endpoint = `${apiUrl}/v1/ingest/cairn`;
|
|
6877
7912
|
const job = new JobReporter(doFetch, apiUrl, token, "ingest");
|
|
6878
|
-
await job.open(
|
|
7913
|
+
await job.open(path21.basename(path21.resolve(dir)), payload.cases.length);
|
|
6879
7914
|
try {
|
|
6880
7915
|
const result = await upload();
|
|
6881
7916
|
await job.uploaded({ subjectId: result.runId || void 0 });
|
|
@@ -6934,12 +7969,12 @@ function formatIngestResult(r) {
|
|
|
6934
7969
|
return `${lines.join("\n")}
|
|
6935
7970
|
`;
|
|
6936
7971
|
}
|
|
6937
|
-
var
|
|
7972
|
+
var path21, IngestNotLoggedInError, IngestTokenRejectedError, IngestNetworkError, IngestHttpError;
|
|
6938
7973
|
var init_ingest = __esm({
|
|
6939
7974
|
"src/cli/commands/ingest.ts"() {
|
|
6940
7975
|
"use strict";
|
|
6941
7976
|
init_cjs_shims();
|
|
6942
|
-
|
|
7977
|
+
path21 = __toESM(require("path"), 1);
|
|
6943
7978
|
init_api_url();
|
|
6944
7979
|
init_credentials();
|
|
6945
7980
|
init_cairn_artifact();
|
|
@@ -7374,10 +8409,10 @@ function testsOfListing(listing) {
|
|
|
7374
8409
|
const annotated = spec.tests?.flatMap((t) => t.annotations ?? []).find(
|
|
7375
8410
|
(a) => a.type === "PluneId" && a.description !== void 0 && a.description !== ""
|
|
7376
8411
|
)?.description;
|
|
7377
|
-
const
|
|
8412
|
+
const id2 = annotated ?? TOKEN4.exec(spec.title)?.[1];
|
|
7378
8413
|
const pathTitle = [...titles, spec.title].join("#");
|
|
7379
8414
|
if (!found.has(pathTitle))
|
|
7380
|
-
found.set(pathTitle, { pathTitle, ...
|
|
8415
|
+
found.set(pathTitle, { pathTitle, ...id2 === void 0 ? {} : { id: id2 } });
|
|
7381
8416
|
}
|
|
7382
8417
|
for (const child of suite.suites ?? []) walk2(child, [...titles, child.title]);
|
|
7383
8418
|
};
|
|
@@ -7814,10 +8849,10 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7814
8849
|
}
|
|
7815
8850
|
});
|
|
7816
8851
|
runCommand.command("finish").argument("<id>", "The run to close").description("Close a platform run \u2014 what the reporter tells you to do for a run left open").option("--terminate", "Record the run as cut short rather than finished", false).option("--reason <text>", "Why, for a terminated run").option("--fail-on-failures", FAIL_ON_FAILURES, false).action(
|
|
7817
|
-
async (
|
|
8852
|
+
async (id2, options, command) => {
|
|
7818
8853
|
try {
|
|
7819
8854
|
const { handleRunFinish: handleRunFinish2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
7820
|
-
const code = await handleRunFinish2(
|
|
8855
|
+
const code = await handleRunFinish2(id2, {
|
|
7821
8856
|
terminate: options.terminate,
|
|
7822
8857
|
...options.reason !== void 0 ? { reason: options.reason } : {},
|
|
7823
8858
|
...options.failOnFailures ? { failOnFailures: true } : {}
|
|
@@ -7846,10 +8881,10 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7846
8881
|
runCommand.command("delete").argument(
|
|
7847
8882
|
"<id>",
|
|
7848
8883
|
"The run id \u2014 the one `plune run import` printed, or the one in the dashboard URL"
|
|
7849
|
-
).description("Delete a run and everything it produced \u2014 recoverable for 30 days").action(async (
|
|
8884
|
+
).description("Delete a run and everything it produced \u2014 recoverable for 30 days").action(async (id2, _options, command) => {
|
|
7850
8885
|
try {
|
|
7851
8886
|
const { handleRunDelete: handleRunDelete2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
7852
|
-
await handleRunDelete2(
|
|
8887
|
+
await handleRunDelete2(id2);
|
|
7853
8888
|
} catch (err) {
|
|
7854
8889
|
await failRunCommand(err, verboseOf(command));
|
|
7855
8890
|
}
|
|
@@ -7987,7 +9022,11 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7987
9022
|
failUnexpected(err, globals.verbose === true);
|
|
7988
9023
|
}
|
|
7989
9024
|
});
|
|
7990
|
-
program.command("diff").description("Compare two `plune run --format json` outputs and report regressions").argument("<baseline>", "Path to the baseline run JSON (e.g. from main)").argument("<current>", "Path to the current run JSON (e.g. from the PR)").option("--format <fmt>", "console | json | markdown", "console").option(
|
|
9025
|
+
program.command("diff").description("Compare two `plune run --format json` outputs and report regressions").argument("<baseline>", "Path to the baseline run JSON (e.g. from main)").argument("<current>", "Path to the current run JSON (e.g. from the PR)").option("--format <fmt>", "console | json | markdown", "console").option(
|
|
9026
|
+
"--fail-on-regression",
|
|
9027
|
+
"Exit 1 when a pass\u2192fail regression is detected, 3 when none is but an eval was not measured",
|
|
9028
|
+
false
|
|
9029
|
+
).option("-o, --output <path>", "Write the diff to a file instead of stdout").action(
|
|
7991
9030
|
async (baseline, current, options, command) => {
|
|
7992
9031
|
const verbose = command.optsWithGlobals().verbose === true;
|
|
7993
9032
|
const [{ handleDiff: handleDiff2, DiffInputError: DiffInputError2 }, { renderDiff: renderDiff2 }] = await Promise.all([
|
|
@@ -7997,7 +9036,9 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7997
9036
|
try {
|
|
7998
9037
|
const diff = handleDiff2({ baselinePath: baseline, currentPath: current });
|
|
7999
9038
|
writeReport(renderDiff2(diff, toFormat(options.format)), options.output);
|
|
8000
|
-
process.exitCode =
|
|
9039
|
+
process.exitCode = 0;
|
|
9040
|
+
if (options.failOnRegression && diff.summary.hasRegression) process.exitCode = 1;
|
|
9041
|
+
else if (options.failOnRegression && diff.summary.unmeasured > 0) process.exitCode = 3;
|
|
8001
9042
|
} catch (err) {
|
|
8002
9043
|
if (err instanceof DiffInputError2) {
|
|
8003
9044
|
process.stderr.write(err.message + "\n");
|
|
@@ -8035,10 +9076,10 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
8035
9076
|
const { handleLogin: handleLogin2, reportLoginFailure: reportLoginFailure2 } = await Promise.resolve().then(() => (init_login(), login_exports));
|
|
8036
9077
|
try {
|
|
8037
9078
|
const token = options.token ?? await readTokenFromStdin();
|
|
8038
|
-
const { path:
|
|
9079
|
+
const { path: path22, verified } = await handleLogin2({ token, skipVerify: options.skipVerify });
|
|
8039
9080
|
process.stdout.write(
|
|
8040
|
-
verified ? `Logged in. Token checked against the API and saved to ${
|
|
8041
|
-
` : `Token saved to ${
|
|
9081
|
+
verified ? `Logged in. Token checked against the API and saved to ${path22}
|
|
9082
|
+
` : `Token saved to ${path22} WITHOUT being checked (--skip-verify).
|
|
8042
9083
|
`
|
|
8043
9084
|
);
|
|
8044
9085
|
} catch (err) {
|
|
@@ -8055,27 +9096,46 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
8055
9096
|
const verbose = command.optsWithGlobals().verbose === true;
|
|
8056
9097
|
const { handleLogout: handleLogout2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
|
|
8057
9098
|
try {
|
|
8058
|
-
const { removed, path:
|
|
9099
|
+
const { removed, path: path22 } = handleLogout2();
|
|
8059
9100
|
process.stdout.write(
|
|
8060
|
-
removed ? `Logged out. Removed ${
|
|
9101
|
+
removed ? `Logged out. Removed ${path22}
|
|
8061
9102
|
` : "Not logged in \u2014 nothing to remove.\n"
|
|
8062
9103
|
);
|
|
8063
9104
|
} catch (err) {
|
|
8064
9105
|
failUnexpected(err, verbose);
|
|
8065
9106
|
}
|
|
8066
9107
|
});
|
|
9108
|
+
program.command("mcp").description("Plune tools for AI agents over the Model Context Protocol").command("serve").description("Serve the Plune tools over MCP stdio, for Claude Code, Codex and other MCP clients").option("--project <id>", "Project the tools work in (default: PLUNE_PROJECT, else the token's own project)").option("--read-only", "Offer only the read tools, as for a read token", false).action(async (options, command) => {
|
|
9109
|
+
const verbose = command.optsWithGlobals().verbose === true;
|
|
9110
|
+
const { handleMcpServe: handleMcpServe2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
|
|
9111
|
+
const { NotLoggedInError: NotLoggedInError2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
9112
|
+
try {
|
|
9113
|
+
await handleMcpServe2(
|
|
9114
|
+
{ project: options.project, readOnly: options.readOnly },
|
|
9115
|
+
{ version: pkg.version }
|
|
9116
|
+
);
|
|
9117
|
+
} catch (err) {
|
|
9118
|
+
if (err instanceof NotLoggedInError2) {
|
|
9119
|
+
process.stderr.write(`plune: ${err.message}
|
|
9120
|
+
`);
|
|
9121
|
+
process.exitCode = 2;
|
|
9122
|
+
return;
|
|
9123
|
+
}
|
|
9124
|
+
failUnexpected(err, verbose);
|
|
9125
|
+
}
|
|
9126
|
+
});
|
|
8067
9127
|
program.command("sync").description("Upload your latest local run to the Plune platform (POST /v1/runs)").option("--file <path>", "Sync a specific run JSON instead of .plune/last-run.json").option("--fail-on-failures", `${FAIL_ON_FAILURES}; read with one GET of the run's summary after the upload`, false).action(async (options, command) => {
|
|
8068
9128
|
const globals = command.optsWithGlobals();
|
|
8069
9129
|
const verbose = globals.verbose === true;
|
|
8070
9130
|
const { handleSync: handleSync2, reportSyncFailure: reportSyncFailure2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
8071
9131
|
const cwd = globals.config !== void 0 ? (0, import_node_path6.dirname)((0, import_node_path6.resolve)(globals.config)) : process.cwd();
|
|
8072
9132
|
try {
|
|
8073
|
-
const { id, url, gate, jobProblem } = await handleSync2({
|
|
9133
|
+
const { id: id2, url, gate, jobProblem } = await handleSync2({
|
|
8074
9134
|
cwd,
|
|
8075
9135
|
...options.file !== void 0 ? { file: options.file } : {},
|
|
8076
9136
|
...options.failOnFailures ? { failOnFailures: true } : {}
|
|
8077
9137
|
});
|
|
8078
|
-
process.stdout.write(`Synced run ${
|
|
9138
|
+
process.stdout.write(`Synced run ${id2} \u2192 ${url}
|
|
8079
9139
|
`);
|
|
8080
9140
|
if (jobProblem !== void 0) {
|
|
8081
9141
|
process.stderr.write(`plune: could not report this sync to the job queue (${jobProblem}) \u2014 the run is uploaded.
|
|
@@ -8218,12 +9278,12 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
8218
9278
|
const planCommand = program.command("plan").description("Test plans on the platform \u2014 what a plan collects, as something a runner can take");
|
|
8219
9279
|
planCommand.command("grep").argument("<id>", "The plan").description(
|
|
8220
9280
|
"Print the plan's cases as one --grep pattern for the runner (GET /v1/plans/:id/cases)"
|
|
8221
|
-
).action(async (
|
|
9281
|
+
).action(async (id2, _options, command) => {
|
|
8222
9282
|
const verbose = command.optsWithGlobals().verbose === true;
|
|
8223
9283
|
const { handlePlanGrep: handlePlanGrep2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
|
|
8224
9284
|
const { reportCasesFailure: reportCasesFailure2 } = await Promise.resolve().then(() => (init_cases(), cases_exports));
|
|
8225
9285
|
try {
|
|
8226
|
-
const out = await handlePlanGrep2({ planId:
|
|
9286
|
+
const out = await handlePlanGrep2({ planId: id2 });
|
|
8227
9287
|
process.stdout.write(out.pattern + "\n");
|
|
8228
9288
|
process.stderr.write(
|
|
8229
9289
|
out.cases === 0 ? "plune: the plan matches no case \u2014 the pattern matches nothing.\n" : `plune: ${out.cases} case(s) in the plan, ${out.keyed} with a test key; the rest by title.
|