@plune-ai/cli 0.32.2 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -1
- package/dist/cli.cjs +1503 -173
- package/dist/index.cjs +34 -16
- package/dist/index.d.cts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +34 -16
- 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
|
@@ -70,6 +70,21 @@ var init_api_url = __esm({
|
|
|
70
70
|
}
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
+
// src/importers/min-score.ts
|
|
74
|
+
function parseMinScore(value2, previous = {}) {
|
|
75
|
+
const match = /^([A-Za-z0-9_-]+)=(\d+(?:\.\d+)?)$/.exec(value2.trim());
|
|
76
|
+
if (match === null) throw new Error(`"${value2}" is not category=score (for example performance=90).`);
|
|
77
|
+
const score = Number(match[2]);
|
|
78
|
+
if (score > 100) throw new Error(`"${value2}" \u2014 a Lighthouse score is 0 to 100.`);
|
|
79
|
+
return { ...previous, [match[1]]: score };
|
|
80
|
+
}
|
|
81
|
+
var init_min_score = __esm({
|
|
82
|
+
"src/importers/min-score.ts"() {
|
|
83
|
+
"use strict";
|
|
84
|
+
init_cjs_shims();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
73
88
|
// src/cli/env.ts
|
|
74
89
|
var env_exports = {};
|
|
75
90
|
__export(env_exports, {
|
|
@@ -558,24 +573,24 @@ var init_prices = __esm({
|
|
|
558
573
|
function priceFrom(price, usage3) {
|
|
559
574
|
return usage3.input_tokens / 1e3 * price.input_per_1k_usd + usage3.output_tokens / 1e3 * price.output_per_1k_usd;
|
|
560
575
|
}
|
|
561
|
-
function
|
|
576
|
+
function resolveCostEntry(usage3, model, reportedCostUsd, overrides, deps = {}) {
|
|
562
577
|
const table = deps.table ?? PRICE_TABLE;
|
|
563
578
|
const warn = deps.warn ?? defaultWarn;
|
|
564
579
|
const override = overrides?.[model];
|
|
565
580
|
if (override !== void 0) {
|
|
566
|
-
return priceFrom(override, usage3);
|
|
581
|
+
return { cost_usd: priceFrom(override, usage3), priced: true };
|
|
567
582
|
}
|
|
568
583
|
if (reportedCostUsd !== void 0) {
|
|
569
|
-
return reportedCostUsd;
|
|
584
|
+
return { cost_usd: reportedCostUsd, priced: true };
|
|
570
585
|
}
|
|
571
586
|
const tablePrice = table[model];
|
|
572
587
|
if (tablePrice !== void 0) {
|
|
573
|
-
return priceFrom(tablePrice, usage3);
|
|
588
|
+
return { cost_usd: priceFrom(tablePrice, usage3), priced: true };
|
|
574
589
|
}
|
|
575
590
|
warn(
|
|
576
591
|
`plune: no price for model "${model}" \u2014 reporting cost_usd=0. Set pricing["${model}"] in plune.yaml to track its cost.`
|
|
577
592
|
);
|
|
578
|
-
return 0;
|
|
593
|
+
return { cost_usd: 0, priced: false };
|
|
579
594
|
}
|
|
580
595
|
var defaultWarn;
|
|
581
596
|
var init_cost = __esm({
|
|
@@ -630,7 +645,7 @@ function makeAnthropicProvider(config, env, pricing) {
|
|
|
630
645
|
}
|
|
631
646
|
},
|
|
632
647
|
estimateCost(usage3, reportedCostUsd) {
|
|
633
|
-
return
|
|
648
|
+
return resolveCostEntry(usage3, config.model, reportedCostUsd, pricing);
|
|
634
649
|
}
|
|
635
650
|
};
|
|
636
651
|
}
|
|
@@ -697,7 +712,7 @@ function makeOpenAiCompatibleProvider(opts) {
|
|
|
697
712
|
}
|
|
698
713
|
},
|
|
699
714
|
estimateCost(usage3, reportedCostUsd) {
|
|
700
|
-
return
|
|
715
|
+
return resolveCostEntry(usage3, config.model, reportedCostUsd, pricing);
|
|
701
716
|
}
|
|
702
717
|
};
|
|
703
718
|
}
|
|
@@ -800,9 +815,7 @@ function makeEstimateProvider(config, pricing) {
|
|
|
800
815
|
complete: () => Promise.reject(
|
|
801
816
|
new Error("The --dry-run provider only estimates cost; it never calls a model.")
|
|
802
817
|
),
|
|
803
|
-
estimateCost: (usage3, reportedCostUsd) => (
|
|
804
|
-
cost_usd: resolveCost(usage3, config.model, reportedCostUsd, pricing)
|
|
805
|
-
})
|
|
818
|
+
estimateCost: (usage3, reportedCostUsd) => resolveCostEntry(usage3, config.model, reportedCostUsd, pricing)
|
|
806
819
|
};
|
|
807
820
|
}
|
|
808
821
|
var init_estimate = __esm({
|
|
@@ -1659,18 +1672,18 @@ var init_judge = __esm({
|
|
|
1659
1672
|
});
|
|
1660
1673
|
|
|
1661
1674
|
// src/orchestrator/concurrency.ts
|
|
1662
|
-
async function mapLimit(
|
|
1663
|
-
const results = new Array(
|
|
1675
|
+
async function mapLimit(items2, limit, fn) {
|
|
1676
|
+
const results = new Array(items2.length);
|
|
1664
1677
|
let next = 0;
|
|
1665
1678
|
const worker = async () => {
|
|
1666
|
-
while (next <
|
|
1679
|
+
while (next < items2.length) {
|
|
1667
1680
|
const i = next;
|
|
1668
1681
|
next += 1;
|
|
1669
|
-
results[i] = await fn(
|
|
1682
|
+
results[i] = await fn(items2[i], i);
|
|
1670
1683
|
}
|
|
1671
1684
|
};
|
|
1672
1685
|
const safeLimit = Number.isFinite(limit) && limit >= 1 ? Math.floor(limit) : 1;
|
|
1673
|
-
const workerCount = Math.min(safeLimit,
|
|
1686
|
+
const workerCount = Math.min(safeLimit, items2.length);
|
|
1674
1687
|
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
1675
1688
|
return results;
|
|
1676
1689
|
}
|
|
@@ -1774,6 +1787,12 @@ function toRunError(err) {
|
|
|
1774
1787
|
function estimateUsage(prompt5, maxTokens) {
|
|
1775
1788
|
return { input_tokens: Math.ceil(prompt5.length / 4), output_tokens: maxTokens };
|
|
1776
1789
|
}
|
|
1790
|
+
function pricedField(priced) {
|
|
1791
|
+
return priced === void 0 ? {} : { priced };
|
|
1792
|
+
}
|
|
1793
|
+
function costFields(c) {
|
|
1794
|
+
return { cost_usd: c.cost_usd, ...pricedField(c.priced) };
|
|
1795
|
+
}
|
|
1777
1796
|
async function runRow(p) {
|
|
1778
1797
|
const t0 = p.now();
|
|
1779
1798
|
const prompt5 = resolvePrompt(p.template, p.row.vars);
|
|
@@ -1781,7 +1800,8 @@ async function runRow(p) {
|
|
|
1781
1800
|
const maxTokens = p.providerConfig.max_tokens ?? DEFAULT_MAX_TOKENS2;
|
|
1782
1801
|
if (p.dryRun) {
|
|
1783
1802
|
const est = estimateUsage(prompt5, maxTokens);
|
|
1784
|
-
const
|
|
1803
|
+
const cost = p.provider.estimateCost(est);
|
|
1804
|
+
const usage4 = { ...est, ...costFields(cost) };
|
|
1785
1805
|
return { vars: p.row.vars, output: null, cached: false, usage: usage4, assertions: [] };
|
|
1786
1806
|
}
|
|
1787
1807
|
const key = cacheKey(
|
|
@@ -1819,7 +1839,9 @@ async function runRow(p) {
|
|
|
1819
1839
|
usage3 = {
|
|
1820
1840
|
input_tokens: hit.usage.input_tokens,
|
|
1821
1841
|
output_tokens: hit.usage.output_tokens,
|
|
1822
|
-
cost_usd: 0
|
|
1842
|
+
cost_usd: 0,
|
|
1843
|
+
// A hit costs nothing, but whether that 0 is a known price depends on the model.
|
|
1844
|
+
...pricedField(p.provider.estimateCost(hit.usage).priced)
|
|
1823
1845
|
};
|
|
1824
1846
|
cacheSaved = {
|
|
1825
1847
|
input_tokens: hit.usage.input_tokens,
|
|
@@ -1836,12 +1858,13 @@ async function runRow(p) {
|
|
|
1836
1858
|
prompt_resolved: prompt5
|
|
1837
1859
|
});
|
|
1838
1860
|
output = res.output;
|
|
1861
|
+
const cost = p.provider.estimateCost(res.usage, res.cost_usd);
|
|
1839
1862
|
usage3 = {
|
|
1840
1863
|
input_tokens: res.usage.input_tokens,
|
|
1841
1864
|
output_tokens: res.usage.output_tokens,
|
|
1842
1865
|
// Prefer the provider-reported actual cost (res.cost_usd) when present; resolveCost still
|
|
1843
1866
|
// lets a config pricing override win, else falls back to the table estimate (ADR-PRC01).
|
|
1844
|
-
|
|
1867
|
+
...costFields(cost)
|
|
1845
1868
|
};
|
|
1846
1869
|
if (!p.noCache && isCacheable(res) && answerParses(res.output, p.assertions)) {
|
|
1847
1870
|
p.cache.set(key, res);
|
|
@@ -1858,8 +1881,11 @@ async function runRow(p) {
|
|
|
1858
1881
|
}
|
|
1859
1882
|
}
|
|
1860
1883
|
let judgeCost = 0;
|
|
1884
|
+
let judgeUnpriced = false;
|
|
1861
1885
|
const judge = buildJudge(p.provider, p.providerConfig, (u) => {
|
|
1862
|
-
|
|
1886
|
+
const c = p.provider.estimateCost(u);
|
|
1887
|
+
judgeCost += c.cost_usd;
|
|
1888
|
+
if (c.priced === false) judgeUnpriced = true;
|
|
1863
1889
|
});
|
|
1864
1890
|
const records = [];
|
|
1865
1891
|
let assertionError;
|
|
@@ -1884,7 +1910,12 @@ async function runRow(p) {
|
|
|
1884
1910
|
break;
|
|
1885
1911
|
}
|
|
1886
1912
|
}
|
|
1887
|
-
const finalUsage = {
|
|
1913
|
+
const finalUsage = {
|
|
1914
|
+
...usage3,
|
|
1915
|
+
cost_usd: usage3.cost_usd + judgeCost,
|
|
1916
|
+
// A row is priced only when every call in it was; the field stays absent if the provider never said.
|
|
1917
|
+
...judgeUnpriced ? { priced: false } : {}
|
|
1918
|
+
};
|
|
1888
1919
|
return {
|
|
1889
1920
|
vars: p.row.vars,
|
|
1890
1921
|
output,
|
|
@@ -1960,7 +1991,7 @@ async function runOrchestration(config, options, deps) {
|
|
|
1960
1991
|
throw new RunConfigError("--no-cache and --cache-max-age cannot be used together");
|
|
1961
1992
|
}
|
|
1962
1993
|
const started = deps.now();
|
|
1963
|
-
const
|
|
1994
|
+
const dryRun2 = options.dryRun ?? false;
|
|
1964
1995
|
const evalResults = [];
|
|
1965
1996
|
const prepared = selectEvals(config.evals, options.only).map((ev) => {
|
|
1966
1997
|
const template = resolveTemplate(ev, deps.baseDir);
|
|
@@ -1986,7 +2017,7 @@ async function runOrchestration(config, options, deps) {
|
|
|
1986
2017
|
embedder: deps.embedder,
|
|
1987
2018
|
cache: deps.cache,
|
|
1988
2019
|
now: deps.now,
|
|
1989
|
-
dryRun,
|
|
2020
|
+
dryRun: dryRun2,
|
|
1990
2021
|
noCache,
|
|
1991
2022
|
cacheStrict,
|
|
1992
2023
|
...options.cacheMaxAgeMs !== void 0 ? { cacheMaxAgeMs: options.cacheMaxAgeMs } : {}
|
|
@@ -2112,18 +2143,18 @@ __export(run_exports, {
|
|
|
2112
2143
|
function isMockMode(env) {
|
|
2113
2144
|
return env["PLUNE_MOCK_PROVIDER"] === "1";
|
|
2114
2145
|
}
|
|
2115
|
-
function resolveProviderFor(config,
|
|
2146
|
+
function resolveProviderFor(config, dryRun2) {
|
|
2116
2147
|
if (isMockMode(process.env)) return () => makeMockProvider();
|
|
2117
|
-
if (
|
|
2148
|
+
if (dryRun2) return (cfg) => makeEstimateProvider(cfg, config.pricing);
|
|
2118
2149
|
return (cfg) => getProvider(cfg, process.env, config.pricing);
|
|
2119
2150
|
}
|
|
2120
|
-
function buildRealDeps(config, baseDir,
|
|
2151
|
+
function buildRealDeps(config, baseDir, dryRun2) {
|
|
2121
2152
|
const dir = path5.join(baseDir, ".plune");
|
|
2122
2153
|
fs5.mkdirSync(dir, { recursive: true });
|
|
2123
2154
|
return {
|
|
2124
|
-
resolveProvider: resolveProviderFor(config,
|
|
2155
|
+
resolveProvider: resolveProviderFor(config, dryRun2),
|
|
2125
2156
|
embedder: getDefaultEmbedder(),
|
|
2126
|
-
cache:
|
|
2157
|
+
cache: dryRun2 ? NOOP_CACHE : openCache(path5.join(dir, "cache.db")),
|
|
2127
2158
|
now: Date.now,
|
|
2128
2159
|
loadDataset,
|
|
2129
2160
|
baseDir
|
|
@@ -2194,8 +2225,8 @@ function wrap(code, s) {
|
|
|
2194
2225
|
}
|
|
2195
2226
|
function createStyler(color) {
|
|
2196
2227
|
if (!color) {
|
|
2197
|
-
const
|
|
2198
|
-
return { green:
|
|
2228
|
+
const id2 = (s) => s;
|
|
2229
|
+
return { green: id2, red: id2, yellow: id2, dim: id2, bold: id2 };
|
|
2199
2230
|
}
|
|
2200
2231
|
return {
|
|
2201
2232
|
green: (s) => wrap(CODE.green, s),
|
|
@@ -2565,7 +2596,7 @@ function createClient(opts) {
|
|
|
2565
2596
|
}
|
|
2566
2597
|
const base = opts.apiUrl.replace(/\/+$/, "");
|
|
2567
2598
|
const scrub = (text2) => opts.token === "" ? text2 : text2.split(opts.token).join("***");
|
|
2568
|
-
async function send(method,
|
|
2599
|
+
async function send(method, path22, payload, retries = DEFAULT_RETRIES) {
|
|
2569
2600
|
let last = {
|
|
2570
2601
|
kind: "unavailable",
|
|
2571
2602
|
status: null,
|
|
@@ -2576,7 +2607,7 @@ function createClient(opts) {
|
|
|
2576
2607
|
await respectPause(mine);
|
|
2577
2608
|
let res;
|
|
2578
2609
|
try {
|
|
2579
|
-
res = await doFetch(`${base}${
|
|
2610
|
+
res = await doFetch(`${base}${path22}`, {
|
|
2580
2611
|
method,
|
|
2581
2612
|
headers: { "content-type": payload?.type ?? "application/json", authorization: `Bearer ${opts.token}` },
|
|
2582
2613
|
// A DELETE carries none. Sending `"undefined"` as a body is what a naive
|
|
@@ -2615,10 +2646,10 @@ function createClient(opts) {
|
|
|
2615
2646
|
return { ok: false, ...last };
|
|
2616
2647
|
}
|
|
2617
2648
|
return {
|
|
2618
|
-
post: (
|
|
2619
|
-
upload: (
|
|
2620
|
-
del: (
|
|
2621
|
-
get: (
|
|
2649
|
+
post: (path22, body, retries) => send("POST", path22, { body: JSON.stringify(body), type: "application/json" }, retries),
|
|
2650
|
+
upload: (path22, bytes, contentType) => send("POST", path22, { body: bytes, type: contentType }, uploadRetries ?? DEFAULT_RETRIES),
|
|
2651
|
+
del: (path22) => send("DELETE", path22),
|
|
2652
|
+
get: (path22) => send("GET", path22),
|
|
2622
2653
|
async put(url, headers, source) {
|
|
2623
2654
|
const putRetries = uploadRetries ?? 3;
|
|
2624
2655
|
let last = { ok: false, status: null, code: "network" };
|
|
@@ -2732,8 +2763,8 @@ function bounded(env, name, range, warnings) {
|
|
|
2732
2763
|
function list(env, name) {
|
|
2733
2764
|
const raw = value(env, name);
|
|
2734
2765
|
if (raw === void 0) return void 0;
|
|
2735
|
-
const
|
|
2736
|
-
return
|
|
2766
|
+
const items2 = raw.split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
2767
|
+
return items2.length === 0 ? void 0 : items2;
|
|
2737
2768
|
}
|
|
2738
2769
|
function readEnv(env = process.env) {
|
|
2739
2770
|
const externalKey = value(env, "PLUNE_RUN");
|
|
@@ -2899,12 +2930,12 @@ function machineless(raw, repoRoot, home) {
|
|
|
2899
2930
|
if (repoRoot === void 0) text2 = text2.replace(ANY_HOME, "~");
|
|
2900
2931
|
return text2;
|
|
2901
2932
|
}
|
|
2902
|
-
function pathIn(
|
|
2903
|
-
const parts =
|
|
2933
|
+
function pathIn(path22) {
|
|
2934
|
+
const parts = path22.split(/[\\/]+/);
|
|
2904
2935
|
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
2905
2936
|
if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
|
|
2906
2937
|
const joined = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
|
|
2907
|
-
const escaped = /^\/(?!\/)/.test(
|
|
2938
|
+
const escaped = /^\/(?!\/)/.test(path22) ? joined.replace("[\\\\/]+", "\\\\*\\/") : joined;
|
|
2908
2939
|
const url = "(?:(?:file|webpack|webpack-internal):(?:\\\\*\\/){2,3})?";
|
|
2909
2940
|
return new RegExp(`${url}${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
|
|
2910
2941
|
}
|
|
@@ -2950,12 +2981,12 @@ function placeOf(error, testFile) {
|
|
|
2950
2981
|
}
|
|
2951
2982
|
function fileOf(raw) {
|
|
2952
2983
|
if (!raw.startsWith("file://")) return raw;
|
|
2953
|
-
let
|
|
2984
|
+
let path22 = raw.slice("file://".length);
|
|
2954
2985
|
try {
|
|
2955
|
-
|
|
2986
|
+
path22 = decodeURIComponent(path22);
|
|
2956
2987
|
} catch {
|
|
2957
2988
|
}
|
|
2958
|
-
return /^\/[A-Za-z]:\//.test(
|
|
2989
|
+
return /^\/[A-Za-z]:\//.test(path22) ? path22.slice(1) : path22;
|
|
2959
2990
|
}
|
|
2960
2991
|
function fromRoot(place, root) {
|
|
2961
2992
|
if (place === void 0 || root === void 0 || !Number.isInteger(place.line) || place.line < 1) return void 0;
|
|
@@ -2995,14 +3026,14 @@ var init_failure_detail = __esm({
|
|
|
2995
3026
|
FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
2996
3027
|
FRAME_MAX = 8192;
|
|
2997
3028
|
firstLine = (text2) => (0, import_node_util.stripVTControlCharacters)(text2).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
2998
|
-
normal = (
|
|
3029
|
+
normal = (path22) => import_node_path2.posix.normalize(path22.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
2999
3030
|
}
|
|
3000
3031
|
});
|
|
3001
3032
|
|
|
3002
3033
|
// src/reporter-core/files-batch.ts
|
|
3003
|
-
async function sha256File(
|
|
3034
|
+
async function sha256File(path22) {
|
|
3004
3035
|
const hash = (0, import_node_crypto2.createHash)("sha256");
|
|
3005
|
-
for await (const part of (0, import_node_fs4.createReadStream)(
|
|
3036
|
+
for await (const part of (0, import_node_fs4.createReadStream)(path22)) hash.update(part);
|
|
3006
3037
|
return hash.digest("hex");
|
|
3007
3038
|
}
|
|
3008
3039
|
var import_node_crypto2, import_node_fs4, FILES_BATCH_MAX, DIRECT_BATCH_BYTES, BATCH_PIPELINES, sha256Bytes, BUCKET_CODES;
|
|
@@ -3109,9 +3140,9 @@ function packed(results) {
|
|
|
3109
3140
|
if (part.length > 0) out.push(part);
|
|
3110
3141
|
return out;
|
|
3111
3142
|
}
|
|
3112
|
-
function chunk(
|
|
3143
|
+
function chunk(items2, size) {
|
|
3113
3144
|
const out = [];
|
|
3114
|
-
for (let i = 0; i <
|
|
3145
|
+
for (let i = 0; i < items2.length; i += size) out.push(items2.slice(i, i + size));
|
|
3115
3146
|
return out;
|
|
3116
3147
|
}
|
|
3117
3148
|
function warnIfDropped(sent, run, log) {
|
|
@@ -3614,15 +3645,15 @@ async function startRun(passed, declared2) {
|
|
|
3614
3645
|
await complete(toComplete);
|
|
3615
3646
|
await sendDirect(directs);
|
|
3616
3647
|
}
|
|
3617
|
-
async function complete(
|
|
3618
|
-
for (let round = 0;
|
|
3648
|
+
async function complete(items2) {
|
|
3649
|
+
for (let round = 0; items2.length > 0; round += 1) {
|
|
3619
3650
|
const out = await gated(() => {
|
|
3620
3651
|
requests += 1;
|
|
3621
|
-
return client.post("/v1/files/complete-batch", { ids:
|
|
3652
|
+
return client.post("/v1/files/complete-batch", { ids: items2.map((i) => i.id) }, uploadRetries);
|
|
3622
3653
|
});
|
|
3623
3654
|
if (routeMissing(out)) {
|
|
3624
3655
|
await Promise.all(
|
|
3625
|
-
|
|
3656
|
+
items2.map(
|
|
3626
3657
|
(i) => gated(async () => {
|
|
3627
3658
|
requests += 1;
|
|
3628
3659
|
const one = await client.post(`/v1/files/${encodeURIComponent(i.id)}/complete`, {}, uploadRetries);
|
|
@@ -3634,28 +3665,28 @@ async function startRun(passed, declared2) {
|
|
|
3634
3665
|
return;
|
|
3635
3666
|
}
|
|
3636
3667
|
const verdicts = out.ok ? out.body?.items : void 0;
|
|
3637
|
-
if (!out.ok || !Array.isArray(verdicts) || verdicts.length !==
|
|
3638
|
-
for (const i of
|
|
3668
|
+
if (!out.ok || !Array.isArray(verdicts) || verdicts.length !== items2.length) {
|
|
3669
|
+
for (const i of items2) failedToBucket(i.job, out.ok ? "bad_answer" : codeOf2(out));
|
|
3639
3670
|
return;
|
|
3640
3671
|
}
|
|
3641
3672
|
const again = [];
|
|
3642
|
-
|
|
3673
|
+
items2.forEach((i, at) => {
|
|
3643
3674
|
const v = verdicts[at] ?? {};
|
|
3644
3675
|
if (v.status === "ready") countsOf(i.job.kind).uploaded += 1;
|
|
3645
3676
|
else if (v.retryable === true && round < Math.min(uploadRetries, 3)) again.push(i);
|
|
3646
3677
|
else refusedItem(i.job, 0, v.status === "refused" ? v : { code: "bad_answer" });
|
|
3647
3678
|
});
|
|
3648
|
-
|
|
3649
|
-
if (
|
|
3650
|
-
retried +=
|
|
3679
|
+
items2 = again;
|
|
3680
|
+
if (items2.length > 0) {
|
|
3681
|
+
retried += items2.length;
|
|
3651
3682
|
await itemPause(round);
|
|
3652
3683
|
}
|
|
3653
3684
|
}
|
|
3654
3685
|
}
|
|
3655
|
-
async function sendDirect(
|
|
3686
|
+
async function sendDirect(items2) {
|
|
3656
3687
|
const groups = [];
|
|
3657
3688
|
let bytes = 0;
|
|
3658
|
-
for (const item of
|
|
3689
|
+
for (const item of items2) {
|
|
3659
3690
|
const group = groups[groups.length - 1];
|
|
3660
3691
|
if (group === void 0 || group.length >= FILES_BATCH_MAX || bytes + item.size > DIRECT_BATCH_BYTES) {
|
|
3661
3692
|
groups.push([item]);
|
|
@@ -3721,10 +3752,10 @@ async function startRun(passed, declared2) {
|
|
|
3721
3752
|
})
|
|
3722
3753
|
);
|
|
3723
3754
|
}
|
|
3724
|
-
async function upload(
|
|
3725
|
-
if (filesByKey.size === 0 || !Array.isArray(
|
|
3755
|
+
async function upload(items2, filesByKey) {
|
|
3756
|
+
if (filesByKey.size === 0 || !Array.isArray(items2)) return;
|
|
3726
3757
|
const jobs = [];
|
|
3727
|
-
for (const item of
|
|
3758
|
+
for (const item of items2) {
|
|
3728
3759
|
if (item?.status !== "accepted" || typeof item.id !== "string" || typeof item.resultKey !== "string") continue;
|
|
3729
3760
|
for (const file of filesByKey.get(item.resultKey) ?? []) {
|
|
3730
3761
|
const type = mediaTypeOf(file.contentType) ?? (file.path === void 0 ? void 0 : TYPE_BY_EXTENSION[(0, import_node_path3.extname)(file.path).toLowerCase()]);
|
|
@@ -4079,9 +4110,9 @@ function connect(deps) {
|
|
|
4079
4110
|
`))
|
|
4080
4111
|
};
|
|
4081
4112
|
}
|
|
4082
|
-
function runUrl(apiUrl,
|
|
4113
|
+
function runUrl(apiUrl, id2) {
|
|
4083
4114
|
const web = dashboardUrl(apiUrl);
|
|
4084
|
-
return web === void 0 ? `${apiUrl}/v1/runs/${
|
|
4115
|
+
return web === void 0 ? `${apiUrl}/v1/runs/${id2}` : `${web}/runs/${id2}`;
|
|
4085
4116
|
}
|
|
4086
4117
|
async function handleRunStart(options = {}) {
|
|
4087
4118
|
const { client, apiUrl, write } = connect(options);
|
|
@@ -4109,39 +4140,39 @@ async function handleRunStart(options = {}) {
|
|
|
4109
4140
|
}
|
|
4110
4141
|
return result;
|
|
4111
4142
|
}
|
|
4112
|
-
async function handleRunFinish(
|
|
4143
|
+
async function handleRunFinish(id2, options = {}, runnerCode) {
|
|
4113
4144
|
const { client, write } = connect(options);
|
|
4114
4145
|
const event = options.terminate === true ? "terminate" : "finish";
|
|
4115
|
-
const out = await client.post(`/v1/runs/${
|
|
4146
|
+
const out = await client.post(`/v1/runs/${id2}/events`, {
|
|
4116
4147
|
event,
|
|
4117
4148
|
...options.reason !== void 0 ? { reason: options.reason } : {}
|
|
4118
4149
|
});
|
|
4119
4150
|
if (!out.ok) {
|
|
4120
4151
|
throw new RunCommandError(
|
|
4121
|
-
out.status === 404 ? `No run '${
|
|
4152
|
+
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}.`
|
|
4122
4153
|
);
|
|
4123
4154
|
}
|
|
4124
|
-
write(`Run ${
|
|
4155
|
+
write(`Run ${id2} ${event === "finish" ? "finished" : "terminated"}.`);
|
|
4125
4156
|
if (options.failOnFailures !== true) return runnerCode ?? 0;
|
|
4126
4157
|
const summary = out.body?.summary;
|
|
4127
4158
|
const gate = gateExit({ statusEvent: out.body?.run?.statusEvent, counts: summary?.counts, notRun: summary?.notRun }, runnerCode);
|
|
4128
4159
|
write(gate.line);
|
|
4129
4160
|
return gate.code;
|
|
4130
4161
|
}
|
|
4131
|
-
async function handleRunDelete(
|
|
4162
|
+
async function handleRunDelete(id2, options = {}) {
|
|
4132
4163
|
const { client, write } = connect(options);
|
|
4133
|
-
const out = await client.del(`/v1/runs/${encodeURIComponent(
|
|
4164
|
+
const out = await client.del(`/v1/runs/${encodeURIComponent(id2)}`);
|
|
4134
4165
|
if (!out.ok) {
|
|
4135
4166
|
throw new RunCommandError(
|
|
4136
4167
|
out.status === 404 ? (
|
|
4137
4168
|
// Three causes, one answer, and the message names all three because the platform will not:
|
|
4138
4169
|
// a 404 that distinguished "not yours" from "not there" would tell a stranger which ids
|
|
4139
4170
|
// exist. The person holding the terminal is better served by the list than by a guess.
|
|
4140
|
-
`No run '${
|
|
4141
|
-
) : `Could not delete run ${
|
|
4171
|
+
`No run '${id2}' \u2014 check the id, whether the token belongs to the same project, or whether it is already deleted.`
|
|
4172
|
+
) : `Could not delete run ${id2} \u2014 ${out.detail || out.kind}.`
|
|
4142
4173
|
);
|
|
4143
4174
|
}
|
|
4144
|
-
write(`Run ${
|
|
4175
|
+
write(`Run ${id2} deleted, with its results and the review-queue entries it produced.`);
|
|
4145
4176
|
write("Recoverable for 30 days \u2014 an owner of the project can put it back from Settings \u2192 Bin.");
|
|
4146
4177
|
}
|
|
4147
4178
|
async function handleRunExec(options) {
|
|
@@ -4302,7 +4333,9 @@ var init_run_result_schema = __esm({
|
|
|
4302
4333
|
usageSchema = import_zod3.z.object({
|
|
4303
4334
|
input_tokens: import_zod3.z.number(),
|
|
4304
4335
|
output_tokens: import_zod3.z.number(),
|
|
4305
|
-
cost_usd: import_zod3.z.number()
|
|
4336
|
+
cost_usd: import_zod3.z.number(),
|
|
4337
|
+
/** cli#151. Optional and additive — a RunResult written before it validates unchanged. */
|
|
4338
|
+
priced: import_zod3.z.boolean().optional()
|
|
4306
4339
|
});
|
|
4307
4340
|
runErrorSchema = import_zod3.z.object({
|
|
4308
4341
|
code: import_zod3.z.string(),
|
|
@@ -4423,11 +4456,11 @@ var init_job_reporter = __esm({
|
|
|
4423
4456
|
}
|
|
4424
4457
|
async finish(outcome) {
|
|
4425
4458
|
if (this.id === void 0) return;
|
|
4426
|
-
const
|
|
4459
|
+
const id2 = this.id;
|
|
4427
4460
|
this.id = void 0;
|
|
4428
4461
|
await this.send(
|
|
4429
4462
|
"PATCH",
|
|
4430
|
-
`/${encodeURIComponent(
|
|
4463
|
+
`/${encodeURIComponent(id2)}/state`,
|
|
4431
4464
|
outcome.ok ? { state: "completed" } : { state: "failed", reason: outcome.reason.slice(0, JOB_REASON_MAX) }
|
|
4432
4465
|
);
|
|
4433
4466
|
}
|
|
@@ -4519,11 +4552,11 @@ async function handleSync(deps = {}) {
|
|
|
4519
4552
|
throw new SyncHttpError(res.status, detail);
|
|
4520
4553
|
}
|
|
4521
4554
|
const body = await res.json().catch(() => ({}));
|
|
4522
|
-
const
|
|
4523
|
-
const result = { id, url: `${endpoint}/${
|
|
4524
|
-
await job.uploaded({ cursor:
|
|
4555
|
+
const id2 = typeof body.id === "string" ? body.id : "";
|
|
4556
|
+
const result = { id: id2, url: `${endpoint}/${id2}` };
|
|
4557
|
+
await job.uploaded({ cursor: id2 || void 0 });
|
|
4525
4558
|
if (deps.failOnFailures === true) {
|
|
4526
|
-
result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${
|
|
4559
|
+
result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id2}/summary`, bearer));
|
|
4527
4560
|
}
|
|
4528
4561
|
return result;
|
|
4529
4562
|
}
|
|
@@ -4633,10 +4666,10 @@ function platformGet(deps) {
|
|
|
4633
4666
|
const token = (deps.loadToken ?? loadToken)();
|
|
4634
4667
|
if (token === null) throw new NotLoggedInError();
|
|
4635
4668
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
4636
|
-
return async (
|
|
4669
|
+
return async (path22) => {
|
|
4637
4670
|
let res;
|
|
4638
4671
|
try {
|
|
4639
|
-
res = await doFetch(`${apiUrl}${
|
|
4672
|
+
res = await doFetch(`${apiUrl}${path22}`, { headers: { authorization: `Bearer ${token}` } });
|
|
4640
4673
|
} catch {
|
|
4641
4674
|
throw new SyncNetworkError(apiUrl);
|
|
4642
4675
|
}
|
|
@@ -4976,10 +5009,24 @@ var init_attachments = __esm({
|
|
|
4976
5009
|
}
|
|
4977
5010
|
});
|
|
4978
5011
|
|
|
5012
|
+
// src/importers/read-file.ts
|
|
5013
|
+
function readReportFile(file) {
|
|
5014
|
+
const text2 = fs11.readFileSync(file, "utf8");
|
|
5015
|
+
return text2.charCodeAt(0) === 65279 ? text2.slice(1) : text2;
|
|
5016
|
+
}
|
|
5017
|
+
var fs11;
|
|
5018
|
+
var init_read_file = __esm({
|
|
5019
|
+
"src/importers/read-file.ts"() {
|
|
5020
|
+
"use strict";
|
|
5021
|
+
init_cjs_shims();
|
|
5022
|
+
fs11 = __toESM(require("fs"), 1);
|
|
5023
|
+
}
|
|
5024
|
+
});
|
|
5025
|
+
|
|
4979
5026
|
// src/importers/allure.ts
|
|
4980
|
-
function isFolder(
|
|
5027
|
+
function isFolder(path22) {
|
|
4981
5028
|
try {
|
|
4982
|
-
return
|
|
5029
|
+
return fs12.statSync(path22).isDirectory();
|
|
4983
5030
|
} catch {
|
|
4984
5031
|
return false;
|
|
4985
5032
|
}
|
|
@@ -4987,7 +5034,7 @@ function isFolder(path19) {
|
|
|
4987
5034
|
function load(file, maxBytes) {
|
|
4988
5035
|
let size;
|
|
4989
5036
|
try {
|
|
4990
|
-
size =
|
|
5037
|
+
size = fs12.statSync(file).size;
|
|
4991
5038
|
} catch (err) {
|
|
4992
5039
|
throw new AllureReportError(`${printable(file)} \u2014 cannot read \u2014 ${printable(err instanceof Error ? err.message : String(err))}`);
|
|
4993
5040
|
}
|
|
@@ -4996,7 +5043,7 @@ function load(file, maxBytes) {
|
|
|
4996
5043
|
}
|
|
4997
5044
|
let parsed;
|
|
4998
5045
|
try {
|
|
4999
|
-
parsed = JSON.parse(
|
|
5046
|
+
parsed = JSON.parse(readReportFile(file));
|
|
5000
5047
|
} catch (err) {
|
|
5001
5048
|
throw new AllureReportError(`${printable(file)} \u2014 not JSON \u2014 ${printable(err instanceof Error ? err.message : String(err))}`);
|
|
5002
5049
|
}
|
|
@@ -5080,8 +5127,8 @@ function readAllure(dir, missing = [], notes = [], maxBytes = MAX_JSON_BYTES) {
|
|
|
5080
5127
|
const ctx = { folder, lexical, missing, notes, around, direct };
|
|
5081
5128
|
const groups = /* @__PURE__ */ new Map();
|
|
5082
5129
|
tests.forEach((r, i) => {
|
|
5083
|
-
const
|
|
5084
|
-
const g =
|
|
5130
|
+
const id2 = textOf(r.historyId);
|
|
5131
|
+
const g = id2 === "" ? `\0${i}` : id2;
|
|
5085
5132
|
const list2 = groups.get(g);
|
|
5086
5133
|
if (list2 === void 0) groups.set(g, [r]);
|
|
5087
5134
|
else list2.push(r);
|
|
@@ -5182,16 +5229,17 @@ function pendingOf(r, retry, ctx) {
|
|
|
5182
5229
|
...files.length > 0 ? { files } : {}
|
|
5183
5230
|
};
|
|
5184
5231
|
}
|
|
5185
|
-
var
|
|
5232
|
+
var fs12, path12, SOURCE, TOKEN, AllureReportError, RESULT, CONTAINER, MAX_JSON_BYTES, MAX_DEPTH, MAX_TIME, namesIn, timeOf, textOf, labelOf, FAILED, declared, stopOf, compareText;
|
|
5186
5233
|
var init_allure = __esm({
|
|
5187
5234
|
"src/importers/allure.ts"() {
|
|
5188
5235
|
"use strict";
|
|
5189
5236
|
init_cjs_shims();
|
|
5190
|
-
|
|
5237
|
+
fs12 = __toESM(require("fs"), 1);
|
|
5191
5238
|
path12 = __toESM(require("path"), 1);
|
|
5192
5239
|
init_failure_detail();
|
|
5193
5240
|
init_result_key();
|
|
5194
5241
|
init_attachments();
|
|
5242
|
+
init_read_file();
|
|
5195
5243
|
SOURCE = "allure";
|
|
5196
5244
|
TOKEN = /@P([A-Za-z0-9_-]+)/;
|
|
5197
5245
|
AllureReportError = class extends Error {
|
|
@@ -5205,7 +5253,7 @@ var init_allure = __esm({
|
|
|
5205
5253
|
MAX_JSON_BYTES = 64 * 1024 * 1024;
|
|
5206
5254
|
MAX_DEPTH = 256;
|
|
5207
5255
|
MAX_TIME = 864e13;
|
|
5208
|
-
namesIn = (dir, pattern) =>
|
|
5256
|
+
namesIn = (dir, pattern) => fs12.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isFile() && pattern.test(d.name)).map((d) => d.name).sort();
|
|
5209
5257
|
timeOf = (v) => typeof v === "number" && Math.abs(v) <= MAX_TIME ? v : void 0;
|
|
5210
5258
|
textOf = (v) => typeof v === "string" ? v : "";
|
|
5211
5259
|
labelOf = (r, name) => textOf(listOf(r.labels).find((l) => l.name === name)?.value);
|
|
@@ -5492,10 +5540,10 @@ function attemptOf(result, file, ctx) {
|
|
|
5492
5540
|
...typeof at?.file === "string" && typeof at.line === "number" ? { location: { file: at.file, line: at.line, ...typeof at.column === "number" ? { column: at.column } : {} } } : {}
|
|
5493
5541
|
})),
|
|
5494
5542
|
steps: steps(result.steps),
|
|
5495
|
-
attachments: (result.attachments ?? []).map(({ name, contentType, path:
|
|
5543
|
+
attachments: (result.attachments ?? []).map(({ name, contentType, path: path22, body }) => ({
|
|
5496
5544
|
name: name ?? "",
|
|
5497
5545
|
...contentType !== void 0 ? { contentType } : {},
|
|
5498
|
-
...
|
|
5546
|
+
...path22 !== void 0 ? { path: path22 } : {},
|
|
5499
5547
|
// Decoded only where the platform would take it (plune-ai/plune#928): a report of screenshots kept as
|
|
5500
5548
|
// bodies is megabytes of base64 that nothing here will read.
|
|
5501
5549
|
...typeof body === "string" && isTextType(contentType) ? { body: Buffer.from(body, "base64") } : {}
|
|
@@ -5612,6 +5660,193 @@ var init_playwright_json = __esm({
|
|
|
5612
5660
|
}
|
|
5613
5661
|
});
|
|
5614
5662
|
|
|
5663
|
+
// src/importers/k6.ts
|
|
5664
|
+
function isObject(value2) {
|
|
5665
|
+
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
5666
|
+
}
|
|
5667
|
+
function isK6Summary(parsed) {
|
|
5668
|
+
if (!isObject(parsed) || !isObject(parsed["metrics"])) return false;
|
|
5669
|
+
if (isObject(parsed["root_group"])) return true;
|
|
5670
|
+
return Object.values(parsed["metrics"]).some((m) => isObject(m) && (typeof m["type"] === "string" || isObject(m["thresholds"])));
|
|
5671
|
+
}
|
|
5672
|
+
function passedOf(verdict) {
|
|
5673
|
+
if (isObject(verdict) && typeof verdict["ok"] === "boolean") return verdict["ok"];
|
|
5674
|
+
if (typeof verdict === "boolean") return !verdict;
|
|
5675
|
+
return void 0;
|
|
5676
|
+
}
|
|
5677
|
+
function measuredOf(metric) {
|
|
5678
|
+
const values = isObject(metric.values) ? metric.values : metric;
|
|
5679
|
+
const unit = metric.contains === "time" ? " ms" : "";
|
|
5680
|
+
return Object.entries(values).filter(([name, v]) => name !== "thresholds" && typeof v === "number").map(([name, v]) => `${name}=${Math.round(v * 100) / 100}${unit}`).join(", ");
|
|
5681
|
+
}
|
|
5682
|
+
function readK6(source, file) {
|
|
5683
|
+
let parsed;
|
|
5684
|
+
try {
|
|
5685
|
+
parsed = JSON.parse(source);
|
|
5686
|
+
} catch (err) {
|
|
5687
|
+
throw new JsonReportError(file, `not JSON \u2014 ${printable(err instanceof Error ? err.message : String(err))}`);
|
|
5688
|
+
}
|
|
5689
|
+
if (!isObject(parsed) || !isObject(parsed["metrics"])) {
|
|
5690
|
+
throw new JsonReportError(file, 'no "metrics" object \u2014 is this a k6 summary (k6 run --summary-export, or the data handleSummary receives)?');
|
|
5691
|
+
}
|
|
5692
|
+
const specRef = path13.basename(file);
|
|
5693
|
+
const out = [];
|
|
5694
|
+
for (const [name, raw] of Object.entries(parsed["metrics"])) {
|
|
5695
|
+
if (!isObject(raw) || !isObject(raw["thresholds"])) continue;
|
|
5696
|
+
const metric = raw;
|
|
5697
|
+
const measured = measuredOf(metric);
|
|
5698
|
+
for (const [expression, verdict] of Object.entries(metric.thresholds)) {
|
|
5699
|
+
const passed = passedOf(verdict);
|
|
5700
|
+
const status = passed === true ? "passed" : "failed";
|
|
5701
|
+
const said = passed === void 0 ? "no verdict in the summary" : status;
|
|
5702
|
+
const value2 = `${SOURCE4}#${specRef}#${name}#${expression}`;
|
|
5703
|
+
out.push({
|
|
5704
|
+
resultKey: resultKey(value2, 0),
|
|
5705
|
+
keys: [{ kind: "path-title", value: value2 }],
|
|
5706
|
+
source: SOURCE4,
|
|
5707
|
+
rawStatus: status,
|
|
5708
|
+
title: `${name} ${expression}`,
|
|
5709
|
+
specRef,
|
|
5710
|
+
errorContext: `k6 threshold "${expression}" on ${name}: ${said}.${measured !== "" ? ` Measured: ${measured}.` : ""}`
|
|
5711
|
+
});
|
|
5712
|
+
}
|
|
5713
|
+
}
|
|
5714
|
+
if (out.length === 0) {
|
|
5715
|
+
throw new JsonReportError(
|
|
5716
|
+
file,
|
|
5717
|
+
"k6 defined no thresholds, so there is nothing to pass or fail. Add thresholds to the script's options (for example http_req_duration: ['p(95)<500']) and run it again."
|
|
5718
|
+
);
|
|
5719
|
+
}
|
|
5720
|
+
return out;
|
|
5721
|
+
}
|
|
5722
|
+
var path13, SOURCE4;
|
|
5723
|
+
var init_k6 = __esm({
|
|
5724
|
+
"src/importers/k6.ts"() {
|
|
5725
|
+
"use strict";
|
|
5726
|
+
init_cjs_shims();
|
|
5727
|
+
path13 = __toESM(require("path"), 1);
|
|
5728
|
+
init_failure_detail();
|
|
5729
|
+
init_result_key();
|
|
5730
|
+
init_playwright_json();
|
|
5731
|
+
SOURCE4 = "k6";
|
|
5732
|
+
}
|
|
5733
|
+
});
|
|
5734
|
+
|
|
5735
|
+
// src/importers/lighthouse.ts
|
|
5736
|
+
function isObject2(value2) {
|
|
5737
|
+
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
5738
|
+
}
|
|
5739
|
+
function isLighthouseReport(parsed) {
|
|
5740
|
+
return isObject2(parsed) && typeof parsed["lighthouseVersion"] === "string" && isObject2(parsed["categories"]);
|
|
5741
|
+
}
|
|
5742
|
+
function idOf(key, category) {
|
|
5743
|
+
return typeof category.id === "string" && category.id !== "" ? category.id : key;
|
|
5744
|
+
}
|
|
5745
|
+
function executionOf2(report) {
|
|
5746
|
+
const started = typeof report["fetchTime"] === "string" ? new Date(report["fetchTime"]) : void 0;
|
|
5747
|
+
if (started === void 0 || Number.isNaN(started.getTime())) return void 0;
|
|
5748
|
+
const timing = report["timing"];
|
|
5749
|
+
const total = isObject2(timing) && typeof timing["total"] === "number" && timing["total"] >= 0 ? Math.round(timing["total"]) : 0;
|
|
5750
|
+
return {
|
|
5751
|
+
startedAt: started.toISOString(),
|
|
5752
|
+
finishedAt: new Date(started.getTime() + total).toISOString(),
|
|
5753
|
+
durationMs: total,
|
|
5754
|
+
retry: 0
|
|
5755
|
+
};
|
|
5756
|
+
}
|
|
5757
|
+
function runtimeErrorOf(report) {
|
|
5758
|
+
const err = report["runtimeError"];
|
|
5759
|
+
if (!isObject2(err)) return void 0;
|
|
5760
|
+
const code = typeof err["code"] === "string" ? err["code"] : void 0;
|
|
5761
|
+
if (code === "NO_ERROR") return void 0;
|
|
5762
|
+
const message = typeof err["message"] === "string" ? err["message"] : void 0;
|
|
5763
|
+
if (code === void 0 && message === void 0) return void 0;
|
|
5764
|
+
return `Lighthouse stopped with ${code ?? "a runtime error"}${message !== void 0 ? `: ${message}` : ""}`;
|
|
5765
|
+
}
|
|
5766
|
+
function readLighthouse(source, file, minScores = {}) {
|
|
5767
|
+
let parsed;
|
|
5768
|
+
try {
|
|
5769
|
+
parsed = JSON.parse(source);
|
|
5770
|
+
} catch (err) {
|
|
5771
|
+
throw new JsonReportError(file, `not JSON \u2014 ${printable(err instanceof Error ? err.message : String(err))}`);
|
|
5772
|
+
}
|
|
5773
|
+
if (!isLighthouseReport(parsed)) {
|
|
5774
|
+
throw new JsonReportError(file, 'no "lighthouseVersion" and "categories" \u2014 is this a Lighthouse JSON report (lighthouse <url> --output=json)?');
|
|
5775
|
+
}
|
|
5776
|
+
const report = parsed;
|
|
5777
|
+
const categories = report["categories"];
|
|
5778
|
+
const url = ["requestedUrl", "finalDisplayedUrl", "finalUrl"].map((k) => report[k]).find((u) => typeof u === "string" && u !== "");
|
|
5779
|
+
const settings = report["configSettings"];
|
|
5780
|
+
const formFactor = isObject2(settings) && typeof settings["formFactor"] === "string" && settings["formFactor"] !== "" ? settings["formFactor"] : void 0;
|
|
5781
|
+
const runtimeError = runtimeErrorOf(report);
|
|
5782
|
+
const execution = executionOf2(report);
|
|
5783
|
+
const specRef = path14.basename(file);
|
|
5784
|
+
const known = Object.entries(categories).filter(([, raw]) => isObject2(raw)).map(([key, raw]) => idOf(key, raw));
|
|
5785
|
+
if (known.length > 0) {
|
|
5786
|
+
for (const [id2, score] of Object.entries(minScores)) {
|
|
5787
|
+
if (!known.includes(id2)) {
|
|
5788
|
+
throw new JsonReportError(
|
|
5789
|
+
file,
|
|
5790
|
+
`--min-score ${printable(id2)}=${score} names a category the report does not have; it has ${known.map(printable).join(", ")}.`
|
|
5791
|
+
);
|
|
5792
|
+
}
|
|
5793
|
+
}
|
|
5794
|
+
}
|
|
5795
|
+
const out = [];
|
|
5796
|
+
const result = (id2, passed, message) => {
|
|
5797
|
+
const value2 = `${SOURCE5}#${url ?? ""}#${formFactor !== void 0 ? `${formFactor}#` : ""}${id2}`;
|
|
5798
|
+
const where = [url, formFactor].filter((part) => part !== void 0);
|
|
5799
|
+
out.push({
|
|
5800
|
+
resultKey: resultKey(value2, 0),
|
|
5801
|
+
keys: [{ kind: "path-title", value: value2 }],
|
|
5802
|
+
source: SOURCE5,
|
|
5803
|
+
rawStatus: passed ? "passed" : "failed",
|
|
5804
|
+
title: where.length > 0 ? `${id2} (${where.join(", ")})` : id2,
|
|
5805
|
+
specRef,
|
|
5806
|
+
...execution !== void 0 ? { execution } : {},
|
|
5807
|
+
errorContext: message
|
|
5808
|
+
});
|
|
5809
|
+
};
|
|
5810
|
+
for (const [key, raw] of Object.entries(categories)) {
|
|
5811
|
+
if (!isObject2(raw)) continue;
|
|
5812
|
+
const category = raw;
|
|
5813
|
+
const id2 = idOf(key, category);
|
|
5814
|
+
const name = typeof category.title === "string" && category.title !== "" ? category.title : id2;
|
|
5815
|
+
const bar = (Object.hasOwn(minScores, id2) ? minScores[id2] : void 0) ?? DEFAULT_MIN_SCORE;
|
|
5816
|
+
let passed = false;
|
|
5817
|
+
let message;
|
|
5818
|
+
if (runtimeError !== void 0) {
|
|
5819
|
+
message = runtimeError;
|
|
5820
|
+
} else if (typeof category.score !== "number") {
|
|
5821
|
+
message = `Lighthouse could not compute a ${name} score, so it cannot pass.`;
|
|
5822
|
+
} else {
|
|
5823
|
+
const shown = Math.round(category.score * 100);
|
|
5824
|
+
passed = shown >= bar;
|
|
5825
|
+
message = `Lighthouse ${name} score: ${shown}, ${passed ? "at least" : "below"} the ${bar} needed to pass.`;
|
|
5826
|
+
}
|
|
5827
|
+
result(id2, passed, message);
|
|
5828
|
+
}
|
|
5829
|
+
if (out.length === 0 && runtimeError !== void 0) result("run", false, runtimeError);
|
|
5830
|
+
if (out.length === 0) {
|
|
5831
|
+
throw new JsonReportError(file, "the report has no categories, so there is nothing to pass or fail.");
|
|
5832
|
+
}
|
|
5833
|
+
return out;
|
|
5834
|
+
}
|
|
5835
|
+
var path14, SOURCE5, DEFAULT_MIN_SCORE;
|
|
5836
|
+
var init_lighthouse = __esm({
|
|
5837
|
+
"src/importers/lighthouse.ts"() {
|
|
5838
|
+
"use strict";
|
|
5839
|
+
init_cjs_shims();
|
|
5840
|
+
path14 = __toESM(require("path"), 1);
|
|
5841
|
+
init_failure_detail();
|
|
5842
|
+
init_result_key();
|
|
5843
|
+
init_playwright_json();
|
|
5844
|
+
init_min_score();
|
|
5845
|
+
SOURCE5 = "lighthouse";
|
|
5846
|
+
DEFAULT_MIN_SCORE = 90;
|
|
5847
|
+
}
|
|
5848
|
+
});
|
|
5849
|
+
|
|
5615
5850
|
// src/importers/index.ts
|
|
5616
5851
|
var importers_exports = {};
|
|
5617
5852
|
__export(importers_exports, {
|
|
@@ -5623,19 +5858,40 @@ __export(importers_exports, {
|
|
|
5623
5858
|
detectFormat: () => detectFormat,
|
|
5624
5859
|
isFolder: () => isFolder,
|
|
5625
5860
|
readFolder: () => readFolder,
|
|
5626
|
-
readReport: () => readReport
|
|
5861
|
+
readReport: () => readReport,
|
|
5862
|
+
readReportFile: () => readReportFile
|
|
5627
5863
|
});
|
|
5628
5864
|
function detectFormat(source, file) {
|
|
5629
5865
|
if (looksLikeJUnit(source)) return "junit";
|
|
5866
|
+
const perf = perfFormatOf(source);
|
|
5867
|
+
if (perf !== void 0) return perf;
|
|
5630
5868
|
if (looksLikePlaywrightJson(source)) return "playwright-json";
|
|
5631
5869
|
throw new UnknownFormatError(
|
|
5632
5870
|
file,
|
|
5633
|
-
'no <testsuite
|
|
5871
|
+
'no <testsuite>, no Playwright "suites", no k6 "metrics" and no Lighthouse "lighthouseVersion" \u2014 this is not a report we can read.'
|
|
5634
5872
|
);
|
|
5635
5873
|
}
|
|
5636
|
-
function
|
|
5637
|
-
if (
|
|
5638
|
-
|
|
5874
|
+
function perfFormatOf(source) {
|
|
5875
|
+
if (!/^\s*\{/.test(source) || !(source.includes('"lighthouseVersion"') || source.includes('"metrics"'))) return void 0;
|
|
5876
|
+
let parsed;
|
|
5877
|
+
try {
|
|
5878
|
+
parsed = JSON.parse(source);
|
|
5879
|
+
} catch {
|
|
5880
|
+
return void 0;
|
|
5881
|
+
}
|
|
5882
|
+
if (isLighthouseReport(parsed)) return "lighthouse";
|
|
5883
|
+
if (isK6Summary(parsed)) return "k6";
|
|
5884
|
+
return void 0;
|
|
5885
|
+
}
|
|
5886
|
+
function readFolder(dir, format, options = {}) {
|
|
5887
|
+
if (format === "playwright-json" || format === "k6" || format === "lighthouse") {
|
|
5888
|
+
throw new UnknownFormatError(dir, `this is a folder, and only allure-results or JUnit XML files are read from one, not ${format}.`);
|
|
5889
|
+
}
|
|
5890
|
+
if (options.minScores !== void 0 && Object.keys(options.minScores).length > 0) {
|
|
5891
|
+
throw new JsonReportError(
|
|
5892
|
+
dir,
|
|
5893
|
+
"--min-score is for Lighthouse reports, and this is a folder: there is no score here to hold to it. Drop --min-score for this folder."
|
|
5894
|
+
);
|
|
5639
5895
|
}
|
|
5640
5896
|
const missing = [];
|
|
5641
5897
|
const notes = [];
|
|
@@ -5646,10 +5902,10 @@ function readFolder(dir, format) {
|
|
|
5646
5902
|
const skipped = [];
|
|
5647
5903
|
let reports = 0;
|
|
5648
5904
|
for (const name of namesIn(dir, /\.xml$/)) {
|
|
5649
|
-
const file =
|
|
5905
|
+
const file = path15.join(dir, name);
|
|
5650
5906
|
let source;
|
|
5651
5907
|
try {
|
|
5652
|
-
source =
|
|
5908
|
+
source = readReportFile(file);
|
|
5653
5909
|
} catch {
|
|
5654
5910
|
throw new RunCommandError(`Cannot read ${printable(file)}.`);
|
|
5655
5911
|
}
|
|
@@ -5669,30 +5925,43 @@ function readFolder(dir, format) {
|
|
|
5669
5925
|
}
|
|
5670
5926
|
return { format: "junit", results, missing, notes };
|
|
5671
5927
|
}
|
|
5672
|
-
function readReport(source, file, format) {
|
|
5928
|
+
function readReport(source, file, format, options = {}) {
|
|
5673
5929
|
const chosen2 = format ?? detectFormat(source, file);
|
|
5674
5930
|
const missing = [];
|
|
5675
5931
|
if (chosen2 === "allure") throw new UnknownFormatError(file, "allure-results is a folder, and this is a file.");
|
|
5676
5932
|
const notes = [];
|
|
5933
|
+
if (chosen2 !== "lighthouse" && options.minScores !== void 0 && Object.keys(options.minScores).length > 0) {
|
|
5934
|
+
throw new JsonReportError(
|
|
5935
|
+
file,
|
|
5936
|
+
`--min-score is for Lighthouse reports, and this is a ${chosen2} report: there is no score here to hold to it. Drop --min-score for this file.`
|
|
5937
|
+
);
|
|
5938
|
+
}
|
|
5677
5939
|
if (chosen2 === "junit") return { format: chosen2, results: readJUnit(source, file, missing, notes), missing, notes };
|
|
5940
|
+
if (chosen2 === "k6") return { format: chosen2, results: readK6(source, file), missing, notes };
|
|
5941
|
+
if (chosen2 === "lighthouse") {
|
|
5942
|
+
return { format: chosen2, results: readLighthouse(source, file, options.minScores), missing, notes };
|
|
5943
|
+
}
|
|
5678
5944
|
return { format: chosen2, ...readPlaywrightJson(source, file), missing, notes };
|
|
5679
5945
|
}
|
|
5680
|
-
var
|
|
5946
|
+
var path15, IMPORT_FORMATS, UnknownFormatError;
|
|
5681
5947
|
var init_importers = __esm({
|
|
5682
5948
|
"src/importers/index.ts"() {
|
|
5683
5949
|
"use strict";
|
|
5684
5950
|
init_cjs_shims();
|
|
5685
|
-
|
|
5686
|
-
path13 = __toESM(require("path"), 1);
|
|
5951
|
+
path15 = __toESM(require("path"), 1);
|
|
5687
5952
|
init_run_lifecycle();
|
|
5688
5953
|
init_failure_detail();
|
|
5689
5954
|
init_allure();
|
|
5690
5955
|
init_junit();
|
|
5691
5956
|
init_playwright_json();
|
|
5957
|
+
init_k6();
|
|
5958
|
+
init_lighthouse();
|
|
5959
|
+
init_read_file();
|
|
5692
5960
|
init_xml();
|
|
5693
5961
|
init_playwright_json();
|
|
5694
5962
|
init_allure();
|
|
5695
|
-
|
|
5963
|
+
init_read_file();
|
|
5964
|
+
IMPORT_FORMATS = ["junit", "playwright-json", "allure", "k6", "lighthouse"];
|
|
5696
5965
|
UnknownFormatError = class extends Error {
|
|
5697
5966
|
constructor(file, detail) {
|
|
5698
5967
|
super(`${file} \u2014 ${detail} Say which with --format ${IMPORT_FORMATS.join(" | ")}.`);
|
|
@@ -5728,15 +5997,19 @@ async function handleRunImport(options) {
|
|
|
5728
5997
|
`));
|
|
5729
5998
|
let read;
|
|
5730
5999
|
if (isFolder(options.file)) {
|
|
5731
|
-
read = readFolder(options.file, options.format
|
|
6000
|
+
read = readFolder(options.file, options.format, {
|
|
6001
|
+
...options.minScores !== void 0 ? { minScores: options.minScores } : {}
|
|
6002
|
+
});
|
|
5732
6003
|
} else {
|
|
5733
6004
|
let source;
|
|
5734
6005
|
try {
|
|
5735
|
-
source =
|
|
6006
|
+
source = readReportFile(options.file);
|
|
5736
6007
|
} catch {
|
|
5737
6008
|
throw new RunCommandError(`Cannot read ${printable(options.file)}.`);
|
|
5738
6009
|
}
|
|
5739
|
-
read = readReport(source, options.file, options.format
|
|
6010
|
+
read = readReport(source, options.file, options.format, {
|
|
6011
|
+
...options.minScores !== void 0 ? { minScores: options.minScores } : {}
|
|
6012
|
+
});
|
|
5740
6013
|
}
|
|
5741
6014
|
const { format, results, missing, notes, ciUrl, ci } = read;
|
|
5742
6015
|
if (results.length === 0) {
|
|
@@ -5830,9 +6103,9 @@ async function handleRunImport(options) {
|
|
|
5830
6103
|
for (const line of session.shardLines) write(line);
|
|
5831
6104
|
return result;
|
|
5832
6105
|
}
|
|
5833
|
-
function runUrl2(apiUrl,
|
|
6106
|
+
function runUrl2(apiUrl, id2) {
|
|
5834
6107
|
const web = dashboardUrl(apiUrl);
|
|
5835
|
-
return web === void 0 ? `${apiUrl}/v1/runs/${
|
|
6108
|
+
return web === void 0 ? `${apiUrl}/v1/runs/${id2}` : `${web}/runs/${id2}`;
|
|
5836
6109
|
}
|
|
5837
6110
|
function describe(result, apiUrl) {
|
|
5838
6111
|
const lines = [
|
|
@@ -5885,12 +6158,11 @@ function describe(result, apiUrl) {
|
|
|
5885
6158
|
if (result.runId !== null) lines.push(runUrl2(apiUrl, result.runId));
|
|
5886
6159
|
return lines;
|
|
5887
6160
|
}
|
|
5888
|
-
var
|
|
6161
|
+
var ImportTimeoutError, NEXT;
|
|
5889
6162
|
var init_run_import = __esm({
|
|
5890
6163
|
"src/cli/commands/run-import.ts"() {
|
|
5891
6164
|
"use strict";
|
|
5892
6165
|
init_cjs_shims();
|
|
5893
|
-
fs13 = __toESM(require("fs"), 1);
|
|
5894
6166
|
init_session();
|
|
5895
6167
|
init_failure_detail();
|
|
5896
6168
|
init_importers();
|
|
@@ -5922,10 +6194,10 @@ function isRunResult(v) {
|
|
|
5922
6194
|
}
|
|
5923
6195
|
function handleReport(opts = {}) {
|
|
5924
6196
|
const cwd = opts.cwd ?? process.cwd();
|
|
5925
|
-
const file =
|
|
6197
|
+
const file = path16.join(cwd, ".plune", "last-run.json");
|
|
5926
6198
|
let raw;
|
|
5927
6199
|
try {
|
|
5928
|
-
raw =
|
|
6200
|
+
raw = fs13.readFileSync(file, "utf8");
|
|
5929
6201
|
} catch {
|
|
5930
6202
|
throw new ReportNotFoundError('No saved run found. Run "plune run" first.');
|
|
5931
6203
|
}
|
|
@@ -5940,13 +6212,13 @@ function handleReport(opts = {}) {
|
|
|
5940
6212
|
}
|
|
5941
6213
|
return parsed;
|
|
5942
6214
|
}
|
|
5943
|
-
var
|
|
6215
|
+
var fs13, path16, ReportNotFoundError;
|
|
5944
6216
|
var init_report = __esm({
|
|
5945
6217
|
"src/cli/commands/report.ts"() {
|
|
5946
6218
|
"use strict";
|
|
5947
6219
|
init_cjs_shims();
|
|
5948
|
-
|
|
5949
|
-
|
|
6220
|
+
fs13 = __toESM(require("fs"), 1);
|
|
6221
|
+
path16 = __toESM(require("path"), 1);
|
|
5950
6222
|
ReportNotFoundError = class extends Error {
|
|
5951
6223
|
code = "NO_SAVED_RUN";
|
|
5952
6224
|
constructor(message) {
|
|
@@ -5975,10 +6247,10 @@ function diffRuns(baseline, current) {
|
|
|
5975
6247
|
const baseMap = new Map(baseline.evals.map((e) => [e.id, evalStatus(e)]));
|
|
5976
6248
|
const curMap = new Map(current.evals.map((e) => [e.id, evalStatus(e)]));
|
|
5977
6249
|
const ids = [.../* @__PURE__ */ new Set([...baseMap.keys(), ...curMap.keys()])];
|
|
5978
|
-
const evals = ids.map((
|
|
5979
|
-
const baselineP = baseMap.get(
|
|
5980
|
-
const currentP = curMap.get(
|
|
5981
|
-
return { id, status: classify2(baselineP, currentP), baseline: baselineP, current: currentP };
|
|
6250
|
+
const evals = ids.map((id2) => {
|
|
6251
|
+
const baselineP = baseMap.get(id2) ?? "absent";
|
|
6252
|
+
const currentP = curMap.get(id2) ?? "absent";
|
|
6253
|
+
return { id: id2, status: classify2(baselineP, currentP), baseline: baselineP, current: currentP };
|
|
5982
6254
|
});
|
|
5983
6255
|
const count2 = (s) => evals.filter((e) => e.status === s).length;
|
|
5984
6256
|
const summary = {
|
|
@@ -6016,7 +6288,7 @@ function isRunResult2(v) {
|
|
|
6016
6288
|
function loadRunResult(file, label) {
|
|
6017
6289
|
let raw;
|
|
6018
6290
|
try {
|
|
6019
|
-
raw =
|
|
6291
|
+
raw = fs14.readFileSync(file, "utf8");
|
|
6020
6292
|
} catch {
|
|
6021
6293
|
throw new DiffInputError(`Cannot read ${label} run file: ${file}`);
|
|
6022
6294
|
}
|
|
@@ -6036,12 +6308,12 @@ function handleDiff(opts) {
|
|
|
6036
6308
|
const current = loadRunResult(opts.currentPath, "current");
|
|
6037
6309
|
return diffRuns(baseline, current);
|
|
6038
6310
|
}
|
|
6039
|
-
var
|
|
6311
|
+
var fs14, DiffInputError;
|
|
6040
6312
|
var init_diff2 = __esm({
|
|
6041
6313
|
"src/cli/commands/diff.ts"() {
|
|
6042
6314
|
"use strict";
|
|
6043
6315
|
init_cjs_shims();
|
|
6044
|
-
|
|
6316
|
+
fs14 = __toESM(require("fs"), 1);
|
|
6045
6317
|
init_diff();
|
|
6046
6318
|
DiffInputError = class extends Error {
|
|
6047
6319
|
code = "DIFF_INPUT";
|
|
@@ -6166,8 +6438,8 @@ async function runInitWizard(cwd) {
|
|
|
6166
6438
|
process.exit(1);
|
|
6167
6439
|
return;
|
|
6168
6440
|
}
|
|
6169
|
-
const outputPath =
|
|
6170
|
-
if (
|
|
6441
|
+
const outputPath = path17.join(cwd, "plune.yaml");
|
|
6442
|
+
if (fs15.existsSync(outputPath)) {
|
|
6171
6443
|
const overwrite = await clack.confirm({
|
|
6172
6444
|
message: "plune.yaml already exists. Overwrite?"
|
|
6173
6445
|
});
|
|
@@ -6206,18 +6478,18 @@ async function runInitWizard(cwd) {
|
|
|
6206
6478
|
`;
|
|
6207
6479
|
const spin = clack.spinner();
|
|
6208
6480
|
spin.start("Writing plune.yaml\u2026");
|
|
6209
|
-
|
|
6481
|
+
fs15.writeFileSync(outputPath, header + "\n" + (0, import_yaml2.stringify)(config), "utf8");
|
|
6210
6482
|
spin.stop("plune.yaml created!");
|
|
6211
6483
|
clack.outro("plune.yaml is ready.");
|
|
6212
6484
|
}
|
|
6213
|
-
var clack,
|
|
6485
|
+
var clack, fs15, path17, import_yaml2;
|
|
6214
6486
|
var init_wizard = __esm({
|
|
6215
6487
|
"src/config/init/wizard.ts"() {
|
|
6216
6488
|
"use strict";
|
|
6217
6489
|
init_cjs_shims();
|
|
6218
6490
|
clack = __toESM(require("@clack/prompts"), 1);
|
|
6219
|
-
|
|
6220
|
-
|
|
6491
|
+
fs15 = __toESM(require("fs"), 1);
|
|
6492
|
+
path17 = __toESM(require("path"), 1);
|
|
6221
6493
|
import_yaml2 = require("yaml");
|
|
6222
6494
|
init_errors();
|
|
6223
6495
|
}
|
|
@@ -6225,21 +6497,21 @@ var init_wizard = __esm({
|
|
|
6225
6497
|
|
|
6226
6498
|
// src/cli/templates/index.ts
|
|
6227
6499
|
function writeTemplateFile(cwd, rel, content, force) {
|
|
6228
|
-
const target =
|
|
6229
|
-
if (!force &&
|
|
6500
|
+
const target = path18.join(cwd, rel);
|
|
6501
|
+
if (!force && fs16.existsSync(target)) {
|
|
6230
6502
|
return "skipped";
|
|
6231
6503
|
}
|
|
6232
|
-
|
|
6233
|
-
|
|
6504
|
+
fs16.mkdirSync(path18.dirname(target), { recursive: true });
|
|
6505
|
+
fs16.writeFileSync(target, content, "utf8");
|
|
6234
6506
|
return "written";
|
|
6235
6507
|
}
|
|
6236
|
-
var
|
|
6508
|
+
var fs16, path18, ENV_EXAMPLE_TPL, EXAMPLE_JSONL_TPL, PLUNE_YAML_TPL;
|
|
6237
6509
|
var init_templates = __esm({
|
|
6238
6510
|
"src/cli/templates/index.ts"() {
|
|
6239
6511
|
"use strict";
|
|
6240
6512
|
init_cjs_shims();
|
|
6241
|
-
|
|
6242
|
-
|
|
6513
|
+
fs16 = __toESM(require("fs"), 1);
|
|
6514
|
+
path18 = __toESM(require("path"), 1);
|
|
6243
6515
|
ENV_EXAMPLE_TPL = `# Copy this file to .env and replace the placeholders with your real keys.
|
|
6244
6516
|
# .env is gitignored; .env.example is safe to commit.
|
|
6245
6517
|
# Plune reads whichever key matches your configured provider.
|
|
@@ -6417,16 +6689,1043 @@ var init_logout = __esm({
|
|
|
6417
6689
|
}
|
|
6418
6690
|
});
|
|
6419
6691
|
|
|
6692
|
+
// src/mcp-tools/schemas.ts
|
|
6693
|
+
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;
|
|
6694
|
+
var init_schemas = __esm({
|
|
6695
|
+
"src/mcp-tools/schemas.ts"() {
|
|
6696
|
+
"use strict";
|
|
6697
|
+
init_cjs_shims();
|
|
6698
|
+
import_zod4 = require("zod");
|
|
6699
|
+
BATCH_MAX2 = 100;
|
|
6700
|
+
isHttp = (s) => {
|
|
6701
|
+
try {
|
|
6702
|
+
const u = new URL(s);
|
|
6703
|
+
return u.protocol === "http:" || u.protocol === "https:";
|
|
6704
|
+
} catch {
|
|
6705
|
+
return false;
|
|
6706
|
+
}
|
|
6707
|
+
};
|
|
6708
|
+
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`);
|
|
6709
|
+
id = (what) => import_zod4.z.string().min(1).describe(what);
|
|
6710
|
+
dryRun = import_zod4.z.boolean().optional().describe("true: answer what would be sent, and send nothing");
|
|
6711
|
+
MISSION_STATES = ["draft", "active", "done"];
|
|
6712
|
+
MISSION_DOCUMENTATION = ["unknown", "provided", "none"];
|
|
6713
|
+
MISSION_LINK_KINDS = ["url", "openapi", "repo", "ticket"];
|
|
6714
|
+
missionName = import_zod4.z.string().trim().min(1, "a mission needs a name").max(200);
|
|
6715
|
+
missionSummary = import_zod4.z.string().trim().max(2e3).describe("What the product is, in 3-5 sentences");
|
|
6716
|
+
missionUrl = httpLink("url", 2048).nullable().describe("Where the product runs; null takes it off");
|
|
6717
|
+
missionFields = {
|
|
6718
|
+
name: missionName,
|
|
6719
|
+
summary: missionSummary.optional(),
|
|
6720
|
+
url: missionUrl.optional(),
|
|
6721
|
+
state: import_zod4.z.enum(MISSION_STATES).optional(),
|
|
6722
|
+
documentation: import_zod4.z.enum(MISSION_DOCUMENTATION).optional().describe("Is there documentation: provided (it is among the sources), none, or unknown")
|
|
6723
|
+
};
|
|
6724
|
+
sourceTitle = import_zod4.z.string().trim().min(1).max(200);
|
|
6725
|
+
sourceVersion = import_zod4.z.string().trim().min(1).max(200).describe("A commit, a tag, a revision, an ETag");
|
|
6726
|
+
linkSource = import_zod4.z.strictObject({
|
|
6727
|
+
missionId: id("The mission"),
|
|
6728
|
+
kind: import_zod4.z.enum(MISSION_LINK_KINDS),
|
|
6729
|
+
ref: httpLink("ref", 2048).describe("The link"),
|
|
6730
|
+
title: sourceTitle.optional(),
|
|
6731
|
+
version: sourceVersion.optional(),
|
|
6732
|
+
dry_run: dryRun
|
|
6733
|
+
});
|
|
6734
|
+
fileSource = import_zod4.z.strictObject({
|
|
6735
|
+
missionId: id("The mission"),
|
|
6736
|
+
kind: import_zod4.z.literal("file"),
|
|
6737
|
+
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"),
|
|
6738
|
+
text: import_zod4.z.string().max(1048576).describe("The whole text of the file, UTF-8, at most 1 MiB"),
|
|
6739
|
+
title: sourceTitle.optional(),
|
|
6740
|
+
version: sourceVersion.optional(),
|
|
6741
|
+
dry_run: dryRun
|
|
6742
|
+
});
|
|
6743
|
+
ACCESS_KINDS = ["openapi", "repo", "staging", "har"];
|
|
6744
|
+
questionText = import_zod4.z.string().trim().min(1).max(2e3);
|
|
6745
|
+
questionInput = import_zod4.z.strictObject({
|
|
6746
|
+
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"),
|
|
6747
|
+
text: questionText,
|
|
6748
|
+
missionId: import_zod4.z.string().min(1).optional(),
|
|
6749
|
+
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"),
|
|
6750
|
+
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"),
|
|
6751
|
+
dry_run: dryRun
|
|
6752
|
+
});
|
|
6753
|
+
STRATEGY_SOURCE_KINDS = ["mission-source", "doc-section", "screen", "endpoint", "requirement", "url"];
|
|
6754
|
+
TEST_LEVELS = ["component", "component-integration", "system", "system-integration", "acceptance"];
|
|
6755
|
+
QUALITY_CHARACTERISTICS = [
|
|
6756
|
+
"functional-suitability",
|
|
6757
|
+
"performance-efficiency",
|
|
6758
|
+
"compatibility",
|
|
6759
|
+
"interaction-capability",
|
|
6760
|
+
"reliability",
|
|
6761
|
+
"security",
|
|
6762
|
+
"maintainability",
|
|
6763
|
+
"flexibility",
|
|
6764
|
+
"safety"
|
|
6765
|
+
];
|
|
6766
|
+
TEST_TECHNIQUES = [
|
|
6767
|
+
"equivalence-partitioning",
|
|
6768
|
+
"boundary-value",
|
|
6769
|
+
"decision-table",
|
|
6770
|
+
"state-transition",
|
|
6771
|
+
"statement",
|
|
6772
|
+
"branch",
|
|
6773
|
+
"error-guessing",
|
|
6774
|
+
"exploratory",
|
|
6775
|
+
"checklist"
|
|
6776
|
+
];
|
|
6777
|
+
RISK_SCALE = ["low", "medium", "high"];
|
|
6778
|
+
itemFields = {
|
|
6779
|
+
text: import_zod4.z.string().trim().min(1).max(1e3).describe("Markdown"),
|
|
6780
|
+
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"),
|
|
6781
|
+
assumption: import_zod4.z.literal(true).optional().describe("You assumed it: a person confirms"),
|
|
6782
|
+
waitingOn: import_zod4.z.string().min(1).optional().describe("The id of an open question of this mission it waits for")
|
|
6783
|
+
};
|
|
6784
|
+
strategyItem = import_zod4.z.strictObject(itemFields);
|
|
6785
|
+
items = import_zod4.z.array(strategyItem).min(1).max(25);
|
|
6786
|
+
unique = (values) => import_zod4.z.array(import_zod4.z.enum(values)).min(1).refine((a) => new Set(a).size === a.length, { message: "named once" });
|
|
6787
|
+
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" });
|
|
6788
|
+
share = import_zod4.z.number().int().min(0).max(100);
|
|
6789
|
+
strategyContent = import_zod4.z.strictObject({
|
|
6790
|
+
objectives: items,
|
|
6791
|
+
scope: import_zod4.z.strictObject({ in: items, out: items.describe("What is deliberately not tested, and why") }),
|
|
6792
|
+
approach: items,
|
|
6793
|
+
profile: import_zod4.z.strictObject({
|
|
6794
|
+
levels: unique(TEST_LEVELS),
|
|
6795
|
+
characteristics: unique(QUALITY_CHARACTERISTICS),
|
|
6796
|
+
techniques: unique(TEST_TECHNIQUES),
|
|
6797
|
+
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" }),
|
|
6798
|
+
notes: items
|
|
6799
|
+
}),
|
|
6800
|
+
risks: import_zod4.z.array(
|
|
6801
|
+
import_zod4.z.strictObject({
|
|
6802
|
+
...itemFields,
|
|
6803
|
+
likelihood: import_zod4.z.enum(RISK_SCALE),
|
|
6804
|
+
impact: import_zod4.z.enum(RISK_SCALE),
|
|
6805
|
+
caseIds: linkIds.optional(),
|
|
6806
|
+
requirementIds: linkIds.optional()
|
|
6807
|
+
})
|
|
6808
|
+
).min(1).max(50),
|
|
6809
|
+
entryCriteria: items,
|
|
6810
|
+
exitCriteria: items
|
|
6811
|
+
});
|
|
6812
|
+
SOURCE_REF_KINDS = ["url", "openapi-operation", "requirement", "pasted-text"];
|
|
6813
|
+
proposedCase = import_zod4.z.strictObject({
|
|
6814
|
+
title: import_zod4.z.string().trim().min(1).max(300),
|
|
6815
|
+
steps: import_zod4.z.array(import_zod4.z.string().trim().min(1).max(400)).min(1).max(20),
|
|
6816
|
+
expected: import_zod4.z.string().trim().min(1).max(2e3),
|
|
6817
|
+
execution: import_zod4.z.enum(["auto", "manual"]).optional().describe("auto: can be automated; manual (default): stays with a person"),
|
|
6818
|
+
technique: import_zod4.z.string().min(1).max(32).optional(),
|
|
6819
|
+
priority: import_zod4.z.string().min(1).optional(),
|
|
6820
|
+
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"),
|
|
6821
|
+
rationale: import_zod4.z.string().min(1).max(400).optional().describe("Why the case is worth having")
|
|
6822
|
+
});
|
|
6823
|
+
API_METHODS = ["GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"];
|
|
6824
|
+
proposedEndpoint = import_zod4.z.strictObject({
|
|
6825
|
+
method: import_zod4.z.enum(API_METHODS),
|
|
6826
|
+
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"),
|
|
6827
|
+
source: import_zod4.z.enum(["spec", "code", "traffic"]),
|
|
6828
|
+
trust: import_zod4.z.enum(["confirmed", "derived"]).optional(),
|
|
6829
|
+
summary: import_zod4.z.string().trim().min(1).max(500).optional(),
|
|
6830
|
+
auth: import_zod4.z.array(import_zod4.z.string().trim().min(1).max(100)).max(10).optional(),
|
|
6831
|
+
environment: import_zod4.z.string().trim().min(1).max(100).optional(),
|
|
6832
|
+
seenAt: import_zod4.z.string().optional().describe("ISO 8601 date and time with an offset")
|
|
6833
|
+
});
|
|
6834
|
+
REVIEW_STATES = ["pending", "changes_requested", "approved", "rejected"];
|
|
6835
|
+
REVIEW_KINDS = ["result", "proposal", "discovered", "endpoint", "strategy", "report", "memory_rule"];
|
|
6836
|
+
REJECTION_CATEGORIES = ["duplicate", "incorrect", "out-of-scope", "low-value", "not-testable", "other"];
|
|
6837
|
+
entity = import_zod4.z.looseObject({ id: import_zod4.z.string().optional() });
|
|
6838
|
+
plannedRequest = import_zod4.z.object({
|
|
6839
|
+
method: import_zod4.z.string(),
|
|
6840
|
+
path: import_zod4.z.string(),
|
|
6841
|
+
query: import_zod4.z.record(import_zod4.z.string(), import_zod4.z.string()).optional(),
|
|
6842
|
+
body: import_zod4.z.unknown().optional()
|
|
6843
|
+
});
|
|
6844
|
+
writeOutput = import_zod4.z.object({
|
|
6845
|
+
dryRun: import_zod4.z.boolean(),
|
|
6846
|
+
request: plannedRequest.optional(),
|
|
6847
|
+
result: entity.optional()
|
|
6848
|
+
});
|
|
6849
|
+
batchOutput = import_zod4.z.object({
|
|
6850
|
+
dryRun: import_zod4.z.boolean(),
|
|
6851
|
+
requested: import_zod4.z.number().int(),
|
|
6852
|
+
applied: import_zod4.z.number().int(),
|
|
6853
|
+
requests: import_zod4.z.array(plannedRequest).optional(),
|
|
6854
|
+
results: import_zod4.z.array(
|
|
6855
|
+
import_zod4.z.object({
|
|
6856
|
+
ok: import_zod4.z.boolean(),
|
|
6857
|
+
status: import_zod4.z.number().int().optional(),
|
|
6858
|
+
id: import_zod4.z.string().optional(),
|
|
6859
|
+
error: import_zod4.z.string().optional(),
|
|
6860
|
+
code: import_zod4.z.string().optional()
|
|
6861
|
+
})
|
|
6862
|
+
).optional(),
|
|
6863
|
+
report: import_zod4.z.looseObject({}).optional()
|
|
6864
|
+
});
|
|
6865
|
+
bulkPreview = import_zod4.z.looseObject({
|
|
6866
|
+
total: import_zod4.z.number().int(),
|
|
6867
|
+
confirm: import_zod4.z.string(),
|
|
6868
|
+
expiresInSeconds: import_zod4.z.number().int()
|
|
6869
|
+
});
|
|
6870
|
+
}
|
|
6871
|
+
});
|
|
6872
|
+
|
|
6873
|
+
// src/mcp-tools/contract.ts
|
|
6874
|
+
function pluneTool(name) {
|
|
6875
|
+
const tool = BY_NAME.get(name);
|
|
6876
|
+
if (tool === void 0) throw new Error(`unknown tool: ${name}`);
|
|
6877
|
+
return tool;
|
|
6878
|
+
}
|
|
6879
|
+
function isPluneToolName(name) {
|
|
6880
|
+
return BY_NAME.has(name);
|
|
6881
|
+
}
|
|
6882
|
+
function defaultVisibleTools(scope) {
|
|
6883
|
+
return PLUNE_TOOLS.filter((t) => t.visibility === "core" && !(scope === "read" && t.write)).map((t) => t.name);
|
|
6884
|
+
}
|
|
6885
|
+
var import_zod5, MAX_VISIBLE_TOOLS, define, listPage, missionId, PLUNE_TOOLS, PLUNE_TOOL_NAMES, BY_NAME;
|
|
6886
|
+
var init_contract = __esm({
|
|
6887
|
+
"src/mcp-tools/contract.ts"() {
|
|
6888
|
+
"use strict";
|
|
6889
|
+
init_cjs_shims();
|
|
6890
|
+
import_zod5 = require("zod");
|
|
6891
|
+
init_schemas();
|
|
6892
|
+
MAX_VISIBLE_TOOLS = 20;
|
|
6893
|
+
define = (tool) => tool;
|
|
6894
|
+
listPage = {
|
|
6895
|
+
limit: import_zod5.z.number().int().min(1).max(BATCH_MAX2).optional(),
|
|
6896
|
+
cursor: import_zod5.z.string().min(1).optional().describe("nextCursor of the previous page")
|
|
6897
|
+
};
|
|
6898
|
+
missionId = id("The mission id");
|
|
6899
|
+
PLUNE_TOOLS = [
|
|
6900
|
+
define({
|
|
6901
|
+
name: "plune_project_context",
|
|
6902
|
+
title: "Project context",
|
|
6903
|
+
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.",
|
|
6904
|
+
input: import_zod5.z.strictObject({}),
|
|
6905
|
+
output: import_zod5.z.object({
|
|
6906
|
+
projectId: import_zod5.z.string().nullable(),
|
|
6907
|
+
projects: import_zod5.z.array(entity),
|
|
6908
|
+
suites: import_zod5.z.array(entity),
|
|
6909
|
+
cases: import_zod5.z.object({ total: import_zod5.z.number().int() }),
|
|
6910
|
+
missions: import_zod5.z.array(entity),
|
|
6911
|
+
reviewQueue: import_zod5.z.object({ pending: import_zod5.z.number().int(), more: import_zod5.z.boolean() })
|
|
6912
|
+
}),
|
|
6913
|
+
write: false,
|
|
6914
|
+
visibility: "core",
|
|
6915
|
+
endpoints: ["GET /v1/projects", "GET /v1/suites", "GET /v1/test-cases", "GET /v1/missions", "GET /v1/review-items"]
|
|
6916
|
+
}),
|
|
6917
|
+
define({
|
|
6918
|
+
name: "plune_get",
|
|
6919
|
+
title: "Read one object",
|
|
6920
|
+
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.",
|
|
6921
|
+
input: import_zod5.z.strictObject({ kind: import_zod5.z.enum(["test-case", "run", "review-item"]), id: id("Its id") }),
|
|
6922
|
+
output: import_zod5.z.object({ kind: import_zod5.z.string(), item: entity }),
|
|
6923
|
+
write: false,
|
|
6924
|
+
visibility: "core",
|
|
6925
|
+
endpoints: ["GET /v1/test-cases/:id", "GET /v1/runs/:id", "GET /v1/review-items/:id"]
|
|
6926
|
+
}),
|
|
6927
|
+
define({
|
|
6928
|
+
name: "plune_cases_search",
|
|
6929
|
+
title: "Find test cases",
|
|
6930
|
+
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.",
|
|
6931
|
+
input: import_zod5.z.strictObject({
|
|
6932
|
+
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"]})'),
|
|
6933
|
+
...listPage
|
|
6934
|
+
}),
|
|
6935
|
+
output: import_zod5.z.object({
|
|
6936
|
+
testCases: import_zod5.z.array(entity),
|
|
6937
|
+
total: import_zod5.z.number().int().optional(),
|
|
6938
|
+
matching: import_zod5.z.number().int().optional(),
|
|
6939
|
+
nextCursor: import_zod5.z.string().nullable()
|
|
6940
|
+
}),
|
|
6941
|
+
write: false,
|
|
6942
|
+
visibility: "core",
|
|
6943
|
+
endpoints: ["GET /v1/test-cases?filters"]
|
|
6944
|
+
}),
|
|
6945
|
+
define({
|
|
6946
|
+
name: "plune_review_list",
|
|
6947
|
+
title: "Read the review queue",
|
|
6948
|
+
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.",
|
|
6949
|
+
input: import_zod5.z.strictObject({
|
|
6950
|
+
state: import_zod5.z.enum(REVIEW_STATES).optional(),
|
|
6951
|
+
kinds: import_zod5.z.array(import_zod5.z.enum(REVIEW_KINDS)).min(1).optional().describe("Questions are read with the question tool"),
|
|
6952
|
+
...listPage
|
|
6953
|
+
}),
|
|
6954
|
+
output: import_zod5.z.object({ items: import_zod5.z.array(entity), nextCursor: import_zod5.z.string().nullable() }),
|
|
6955
|
+
write: false,
|
|
6956
|
+
visibility: "core",
|
|
6957
|
+
endpoints: ["GET /v1/review-items"]
|
|
6958
|
+
}),
|
|
6959
|
+
define({
|
|
6960
|
+
name: "plune_review_decide",
|
|
6961
|
+
title: "Decide queue entries",
|
|
6962
|
+
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.",
|
|
6963
|
+
input: import_zod5.z.strictObject({
|
|
6964
|
+
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" }),
|
|
6965
|
+
verdict: import_zod5.z.enum(["approved", "rejected"]),
|
|
6966
|
+
rejectionCategory: import_zod5.z.enum(REJECTION_CATEGORIES).optional().describe("Required to reject"),
|
|
6967
|
+
note: import_zod5.z.string().trim().min(1).max(2e3).optional().describe("Why \u2014 required to reject"),
|
|
6968
|
+
confirm: import_zod5.z.string().min(1).optional().describe("The confirm value of the preview")
|
|
6969
|
+
}),
|
|
6970
|
+
output: import_zod5.z.object({
|
|
6971
|
+
requested: import_zod5.z.number().int(),
|
|
6972
|
+
applied: import_zod5.z.number().int(),
|
|
6973
|
+
preview: bulkPreview.optional(),
|
|
6974
|
+
report: import_zod5.z.looseObject({}).optional()
|
|
6975
|
+
}),
|
|
6976
|
+
write: true,
|
|
6977
|
+
visibility: "core",
|
|
6978
|
+
endpoints: ["POST /v1/review-items/decisions"]
|
|
6979
|
+
}),
|
|
6980
|
+
define({
|
|
6981
|
+
name: "plune_case_propose",
|
|
6982
|
+
title: "Propose test cases",
|
|
6983
|
+
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.",
|
|
6984
|
+
input: import_zod5.z.strictObject({
|
|
6985
|
+
target: import_zod5.z.string().trim().min(1).max(2048).describe("What the cases test: the product address, or the mission name"),
|
|
6986
|
+
cases: import_zod5.z.array(proposedCase).min(1).max(BATCH_MAX2),
|
|
6987
|
+
dry_run: dryRun
|
|
6988
|
+
}),
|
|
6989
|
+
output: batchOutput,
|
|
6990
|
+
write: true,
|
|
6991
|
+
visibility: "core",
|
|
6992
|
+
endpoints: ["POST /v1/ingest/cairn"]
|
|
6993
|
+
}),
|
|
6994
|
+
define({
|
|
6995
|
+
name: "plune_mission_create",
|
|
6996
|
+
title: "Create a mission",
|
|
6997
|
+
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.",
|
|
6998
|
+
input: import_zod5.z.strictObject({ ...missionFields, dry_run: dryRun }),
|
|
6999
|
+
output: writeOutput,
|
|
7000
|
+
write: true,
|
|
7001
|
+
visibility: "core",
|
|
7002
|
+
endpoints: ["POST /v1/missions"]
|
|
7003
|
+
}),
|
|
7004
|
+
define({
|
|
7005
|
+
name: "plune_mission_get",
|
|
7006
|
+
title: "Read a mission",
|
|
7007
|
+
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.",
|
|
7008
|
+
input: import_zod5.z.strictObject({ missionId }),
|
|
7009
|
+
output: import_zod5.z.object({ mission: entity }),
|
|
7010
|
+
write: false,
|
|
7011
|
+
visibility: "core",
|
|
7012
|
+
endpoints: ["GET /v1/missions/:id"]
|
|
7013
|
+
}),
|
|
7014
|
+
define({
|
|
7015
|
+
name: "plune_mission_update",
|
|
7016
|
+
title: "Update a mission",
|
|
7017
|
+
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.",
|
|
7018
|
+
input: import_zod5.z.strictObject({
|
|
7019
|
+
missionId,
|
|
7020
|
+
name: missionFields.name.optional(),
|
|
7021
|
+
summary: missionFields.summary,
|
|
7022
|
+
url: missionFields.url,
|
|
7023
|
+
state: missionFields.state,
|
|
7024
|
+
documentation: missionFields.documentation,
|
|
7025
|
+
dry_run: dryRun
|
|
7026
|
+
}).refine((b) => ["name", "summary", "url", "state", "documentation"].some((k) => b[k] !== void 0), {
|
|
7027
|
+
message: "nothing to change"
|
|
7028
|
+
}),
|
|
7029
|
+
output: writeOutput,
|
|
7030
|
+
write: true,
|
|
7031
|
+
visibility: "core",
|
|
7032
|
+
endpoints: ["PATCH /v1/missions/:id"]
|
|
7033
|
+
}),
|
|
7034
|
+
define({
|
|
7035
|
+
name: "plune_source_add",
|
|
7036
|
+
title: "Add a source to a mission",
|
|
7037
|
+
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.",
|
|
7038
|
+
input: import_zod5.z.strictObject({
|
|
7039
|
+
missionId,
|
|
7040
|
+
kind: import_zod5.z.enum([...MISSION_LINK_KINDS, "file"]),
|
|
7041
|
+
ref: import_zod5.z.string().optional().describe("The link, for url, openapi, repo and ticket"),
|
|
7042
|
+
name: import_zod5.z.string().optional().describe("The file name, for file (.md, .markdown, .txt, .json)"),
|
|
7043
|
+
text: import_zod5.z.string().optional().describe("The file text, for file"),
|
|
7044
|
+
title: import_zod5.z.string().optional(),
|
|
7045
|
+
version: import_zod5.z.string().optional(),
|
|
7046
|
+
dry_run: dryRun
|
|
7047
|
+
}).superRefine((b, ctx) => {
|
|
7048
|
+
if (b.kind === "file" && (b.name === void 0 || b.text === void 0))
|
|
7049
|
+
ctx.addIssue({ code: "custom", message: "a file source carries name and text" });
|
|
7050
|
+
if (b.kind !== "file" && b.ref === void 0) ctx.addIssue({ code: "custom", message: "a link source carries ref" });
|
|
7051
|
+
}),
|
|
7052
|
+
output: writeOutput,
|
|
7053
|
+
write: true,
|
|
7054
|
+
visibility: "core",
|
|
7055
|
+
endpoints: ["POST /v1/missions/:id/sources", "POST /v1/missions/:id/files"]
|
|
7056
|
+
}),
|
|
7057
|
+
define({
|
|
7058
|
+
name: "plune_question_ask",
|
|
7059
|
+
title: "Ask a person",
|
|
7060
|
+
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.",
|
|
7061
|
+
input: questionInput.superRefine((b, ctx) => {
|
|
7062
|
+
if (b.template === "plain" && b.access !== void 0) ctx.addIssue({ code: "custom", message: "access belongs to an access question" });
|
|
7063
|
+
if (b.template === "access" && b.options !== void 0) ctx.addIssue({ code: "custom", message: "options belong to a plain question" });
|
|
7064
|
+
if (b.template === "access" && b.access === void 0) ctx.addIssue({ code: "custom", message: "an access question names the access" });
|
|
7065
|
+
}),
|
|
7066
|
+
output: writeOutput,
|
|
7067
|
+
write: true,
|
|
7068
|
+
visibility: "core",
|
|
7069
|
+
endpoints: ["POST /v1/review-items/questions"]
|
|
7070
|
+
}),
|
|
7071
|
+
define({
|
|
7072
|
+
name: "plune_question_list",
|
|
7073
|
+
title: "Read questions and answers",
|
|
7074
|
+
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.",
|
|
7075
|
+
input: import_zod5.z.strictObject({ missionId: import_zod5.z.string().min(1).optional(), state: import_zod5.z.enum(REVIEW_STATES).optional(), ...listPage }),
|
|
7076
|
+
output: import_zod5.z.object({ questions: import_zod5.z.array(entity), nextCursor: import_zod5.z.string().nullable() }),
|
|
7077
|
+
write: false,
|
|
7078
|
+
visibility: "core",
|
|
7079
|
+
endpoints: ["GET /v1/review-items?kind=question"]
|
|
7080
|
+
}),
|
|
7081
|
+
define({
|
|
7082
|
+
name: "plune_strategy_propose",
|
|
7083
|
+
title: "Propose a test strategy",
|
|
7084
|
+
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.",
|
|
7085
|
+
input: import_zod5.z.strictObject({ missionId, content: strategyContent, dry_run: dryRun }),
|
|
7086
|
+
output: writeOutput,
|
|
7087
|
+
write: true,
|
|
7088
|
+
visibility: "core",
|
|
7089
|
+
endpoints: ["POST /v1/missions/:id/strategy"]
|
|
7090
|
+
}),
|
|
7091
|
+
define({
|
|
7092
|
+
name: "plune_report_request",
|
|
7093
|
+
title: "Request the mission report",
|
|
7094
|
+
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.",
|
|
7095
|
+
input: import_zod5.z.strictObject({ missionId, dry_run: dryRun }),
|
|
7096
|
+
output: writeOutput,
|
|
7097
|
+
write: true,
|
|
7098
|
+
visibility: "core",
|
|
7099
|
+
endpoints: ["POST /v1/missions/:id/report"]
|
|
7100
|
+
}),
|
|
7101
|
+
define({
|
|
7102
|
+
name: "plune_discover_tools",
|
|
7103
|
+
title: "Find more tools",
|
|
7104
|
+
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.",
|
|
7105
|
+
input: import_zod5.z.strictObject({
|
|
7106
|
+
query: import_zod5.z.string().trim().min(1).max(200).optional().describe('Words of the task, e.g. "write results"'),
|
|
7107
|
+
names: import_zod5.z.array(import_zod5.z.string().min(1)).min(1).max(MAX_VISIBLE_TOOLS).optional().describe("Tools to switch on")
|
|
7108
|
+
}),
|
|
7109
|
+
output: import_zod5.z.object({
|
|
7110
|
+
revealed: import_zod5.z.array(import_zod5.z.string()),
|
|
7111
|
+
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() })),
|
|
7112
|
+
visible: import_zod5.z.array(import_zod5.z.string())
|
|
7113
|
+
}),
|
|
7114
|
+
write: false,
|
|
7115
|
+
visibility: "core",
|
|
7116
|
+
endpoints: ["LOCAL /discover"]
|
|
7117
|
+
}),
|
|
7118
|
+
define({
|
|
7119
|
+
name: "plune_cases_update",
|
|
7120
|
+
title: "Edit test cases",
|
|
7121
|
+
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.",
|
|
7122
|
+
input: import_zod5.z.strictObject({
|
|
7123
|
+
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),
|
|
7124
|
+
dry_run: dryRun
|
|
7125
|
+
}),
|
|
7126
|
+
output: batchOutput,
|
|
7127
|
+
write: true,
|
|
7128
|
+
visibility: "discoverable",
|
|
7129
|
+
endpoints: ["PATCH /v1/test-cases/:id"]
|
|
7130
|
+
}),
|
|
7131
|
+
define({
|
|
7132
|
+
name: "plune_run_create",
|
|
7133
|
+
title: "Start a run",
|
|
7134
|
+
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).",
|
|
7135
|
+
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 }),
|
|
7136
|
+
output: writeOutput,
|
|
7137
|
+
write: true,
|
|
7138
|
+
visibility: "discoverable",
|
|
7139
|
+
endpoints: ["POST /v1/runs"]
|
|
7140
|
+
}),
|
|
7141
|
+
define({
|
|
7142
|
+
name: "plune_results_write",
|
|
7143
|
+
title: "Write results into a run",
|
|
7144
|
+
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.",
|
|
7145
|
+
input: import_zod5.z.strictObject({
|
|
7146
|
+
runId: id("The run id"),
|
|
7147
|
+
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"),
|
|
7148
|
+
combination: import_zod5.z.number().int().min(0).optional(),
|
|
7149
|
+
configuration: import_zod5.z.record(import_zod5.z.string(), import_zod5.z.string()).optional(),
|
|
7150
|
+
dry_run: dryRun
|
|
7151
|
+
}),
|
|
7152
|
+
output: batchOutput,
|
|
7153
|
+
write: true,
|
|
7154
|
+
visibility: "discoverable",
|
|
7155
|
+
endpoints: ["POST /v1/runs/:id/results"]
|
|
7156
|
+
}),
|
|
7157
|
+
define({
|
|
7158
|
+
name: "plune_endpoint_propose",
|
|
7159
|
+
title: "Propose API endpoints",
|
|
7160
|
+
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.",
|
|
7161
|
+
input: import_zod5.z.strictObject({ endpoints: import_zod5.z.array(proposedEndpoint).min(1).max(BATCH_MAX2), dry_run: dryRun }),
|
|
7162
|
+
output: batchOutput,
|
|
7163
|
+
write: true,
|
|
7164
|
+
visibility: "discoverable",
|
|
7165
|
+
endpoints: ["POST /v1/review-items/endpoints"]
|
|
7166
|
+
}),
|
|
7167
|
+
define({
|
|
7168
|
+
name: "plune_api_request",
|
|
7169
|
+
title: "Call the REST API directly",
|
|
7170
|
+
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.",
|
|
7171
|
+
input: import_zod5.z.strictObject({
|
|
7172
|
+
method: import_zod5.z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
|
|
7173
|
+
path: import_zod5.z.string().min(1).max(2048).describe("A path under /v1, e.g. /v1/suites"),
|
|
7174
|
+
query: import_zod5.z.record(import_zod5.z.string(), import_zod5.z.string()).optional(),
|
|
7175
|
+
body: import_zod5.z.unknown().optional(),
|
|
7176
|
+
confirm: import_zod5.z.string().min(1).optional().describe("The confirm value of a preview"),
|
|
7177
|
+
dry_run: dryRun
|
|
7178
|
+
}),
|
|
7179
|
+
output: import_zod5.z.object({
|
|
7180
|
+
status: import_zod5.z.number().int().optional(),
|
|
7181
|
+
body: import_zod5.z.unknown().optional(),
|
|
7182
|
+
dryRun: import_zod5.z.boolean().optional(),
|
|
7183
|
+
request: plannedRequest.optional(),
|
|
7184
|
+
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()
|
|
7185
|
+
}),
|
|
7186
|
+
write: true,
|
|
7187
|
+
visibility: "discoverable",
|
|
7188
|
+
endpoints: ["ANY /v1/*"]
|
|
7189
|
+
})
|
|
7190
|
+
];
|
|
7191
|
+
PLUNE_TOOL_NAMES = PLUNE_TOOLS.map((t) => t.name);
|
|
7192
|
+
BY_NAME = new Map(PLUNE_TOOLS.map((t) => [t.name, t]));
|
|
7193
|
+
}
|
|
7194
|
+
});
|
|
7195
|
+
|
|
7196
|
+
// src/mcp-tools/client.ts
|
|
7197
|
+
function bodyField(body, key) {
|
|
7198
|
+
if (body === null || typeof body !== "object") return void 0;
|
|
7199
|
+
const value2 = body[key];
|
|
7200
|
+
return typeof value2 === "string" ? value2 : void 0;
|
|
7201
|
+
}
|
|
7202
|
+
function apiBase(apiUrl) {
|
|
7203
|
+
return apiUrl.replace(/\/+$/, "");
|
|
7204
|
+
}
|
|
7205
|
+
function buildUrl(apiUrl, req) {
|
|
7206
|
+
const url = new URL(apiBase(apiUrl) + req.path);
|
|
7207
|
+
for (const [key, value2] of Object.entries(req.query ?? {})) {
|
|
7208
|
+
for (const v of typeof value2 === "string" ? [value2] : value2) url.searchParams.append(key, v);
|
|
7209
|
+
}
|
|
7210
|
+
return url;
|
|
7211
|
+
}
|
|
7212
|
+
function createPluneApi(opts) {
|
|
7213
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
7214
|
+
const headers = {
|
|
7215
|
+
authorization: `Bearer ${opts.token}`,
|
|
7216
|
+
accept: "application/json",
|
|
7217
|
+
"x-plune-session": opts.sessionId,
|
|
7218
|
+
...opts.projectId === void 0 ? {} : { "x-plune-project": opts.projectId },
|
|
7219
|
+
...opts.userAgent === void 0 ? {} : { "user-agent": opts.userAgent }
|
|
7220
|
+
};
|
|
7221
|
+
return async function request(req) {
|
|
7222
|
+
const url = buildUrl(opts.apiUrl, req);
|
|
7223
|
+
let res;
|
|
7224
|
+
try {
|
|
7225
|
+
res = await doFetch(url.toString(), {
|
|
7226
|
+
method: req.method,
|
|
7227
|
+
headers: req.body === void 0 ? headers : { ...headers, "content-type": "application/json" },
|
|
7228
|
+
...req.body === void 0 ? {} : { body: JSON.stringify(req.body) },
|
|
7229
|
+
// Host pinning (#1283): a redirect elsewhere would carry the token there.
|
|
7230
|
+
redirect: "manual"
|
|
7231
|
+
});
|
|
7232
|
+
} catch {
|
|
7233
|
+
throw new PluneNetworkError(apiBase(opts.apiUrl));
|
|
7234
|
+
}
|
|
7235
|
+
const text2 = await res.text();
|
|
7236
|
+
let body = text2;
|
|
7237
|
+
if (text2 !== "") {
|
|
7238
|
+
try {
|
|
7239
|
+
body = JSON.parse(text2);
|
|
7240
|
+
} catch {
|
|
7241
|
+
}
|
|
7242
|
+
} else {
|
|
7243
|
+
body = void 0;
|
|
7244
|
+
}
|
|
7245
|
+
if (res.status < 200 || res.status >= 300) throw new PluneApiError(res.status, body);
|
|
7246
|
+
return { status: res.status, body };
|
|
7247
|
+
};
|
|
7248
|
+
}
|
|
7249
|
+
var PluneApiError, PluneNetworkError;
|
|
7250
|
+
var init_client2 = __esm({
|
|
7251
|
+
"src/mcp-tools/client.ts"() {
|
|
7252
|
+
"use strict";
|
|
7253
|
+
init_cjs_shims();
|
|
7254
|
+
PluneApiError = class extends Error {
|
|
7255
|
+
constructor(status, body) {
|
|
7256
|
+
const error = bodyField(body, "error");
|
|
7257
|
+
super(error ?? `HTTP ${status}`);
|
|
7258
|
+
this.status = status;
|
|
7259
|
+
this.body = body;
|
|
7260
|
+
this.name = "PluneApiError";
|
|
7261
|
+
}
|
|
7262
|
+
status;
|
|
7263
|
+
body;
|
|
7264
|
+
get code() {
|
|
7265
|
+
return bodyField(this.body, "code");
|
|
7266
|
+
}
|
|
7267
|
+
};
|
|
7268
|
+
PluneNetworkError = class extends Error {
|
|
7269
|
+
constructor(apiUrl) {
|
|
7270
|
+
super(`could not reach Plune at ${apiUrl}`);
|
|
7271
|
+
this.apiUrl = apiUrl;
|
|
7272
|
+
this.name = "PluneNetworkError";
|
|
7273
|
+
}
|
|
7274
|
+
apiUrl;
|
|
7275
|
+
};
|
|
7276
|
+
}
|
|
7277
|
+
});
|
|
7278
|
+
|
|
7279
|
+
// src/mcp-tools/execute.ts
|
|
7280
|
+
function errorOf(err) {
|
|
7281
|
+
if (err instanceof PluneApiError) {
|
|
7282
|
+
if (err.status === 401)
|
|
7283
|
+
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.` };
|
|
7284
|
+
if (err.status === 403 && err.message === READ_ONLY_REFUSAL)
|
|
7285
|
+
return { status: 403, code: "read_only_token", message: `Plune refused (403): ${readScopeMessage("it cannot write")}` };
|
|
7286
|
+
const code = err.code ?? `http_${err.status}`;
|
|
7287
|
+
return { status: err.status, code, message: `Plune refused (${err.status} ${code}): ${err.message}`, details: err.body };
|
|
7288
|
+
}
|
|
7289
|
+
if (err instanceof PluneNetworkError) return { code: "network", message: `${err.message}. Check PLUNE_API_URL and the network.` };
|
|
7290
|
+
throw err;
|
|
7291
|
+
}
|
|
7292
|
+
function isDeletion(req) {
|
|
7293
|
+
if (req.method === "DELETE") return true;
|
|
7294
|
+
if (asRecord(asRecord(req.body)["action"])["kind"] === "delete") return true;
|
|
7295
|
+
return /\/(bulk-delete|purge|empty)$/.test(req.path);
|
|
7296
|
+
}
|
|
7297
|
+
function safePath(apiUrl, raw) {
|
|
7298
|
+
if (!/^\/v1\/[^\\\s]*$/.test(raw) || raw.startsWith("//") || raw.includes("?") || raw.includes("#")) return void 0;
|
|
7299
|
+
if (raw.split("/").some((s) => s === "." || s === ".." || /^%2e/i.test(s))) return void 0;
|
|
7300
|
+
const base = new URL(apiUrl);
|
|
7301
|
+
const url = new URL(raw, base);
|
|
7302
|
+
return url.origin === base.origin && url.pathname.startsWith("/v1/") ? raw : void 0;
|
|
7303
|
+
}
|
|
7304
|
+
function createPluneToolExecutor(opts) {
|
|
7305
|
+
const sessionId = opts.sessionId ?? (0, import_node_crypto5.randomUUID)();
|
|
7306
|
+
const now = opts.now ?? Date.now;
|
|
7307
|
+
const request = createPluneApi({ ...opts, sessionId });
|
|
7308
|
+
let scope = opts.scope;
|
|
7309
|
+
const revealed = /* @__PURE__ */ new Set();
|
|
7310
|
+
const confirms = /* @__PURE__ */ new Map();
|
|
7311
|
+
let proposals = 0;
|
|
7312
|
+
const visibleTools = () => {
|
|
7313
|
+
const core = defaultVisibleTools(scope);
|
|
7314
|
+
const extra = [...revealed].filter((n) => !(scope === "read" && pluneTool(n).write) && !core.includes(n));
|
|
7315
|
+
return [...core, ...extra];
|
|
7316
|
+
};
|
|
7317
|
+
async function call(req) {
|
|
7318
|
+
try {
|
|
7319
|
+
return await request(req);
|
|
7320
|
+
} catch (err) {
|
|
7321
|
+
if (err instanceof PluneApiError && err.status === 403 && err.message === READ_ONLY_REFUSAL) scope = "read";
|
|
7322
|
+
throw err;
|
|
7323
|
+
}
|
|
7324
|
+
}
|
|
7325
|
+
const planned = (req) => withoutUndefined({ method: req.method, path: req.path, query: req.query, body: req.body });
|
|
7326
|
+
async function write(req, dry) {
|
|
7327
|
+
if (dry === true) return { dryRun: true, request: planned(req) };
|
|
7328
|
+
const res = await call(req);
|
|
7329
|
+
return { dryRun: false, result: asRecord(res.body) };
|
|
7330
|
+
}
|
|
7331
|
+
async function batch(reqs, dry, idOf2) {
|
|
7332
|
+
if (dry === true) return { dryRun: true, requested: reqs.length, applied: 0, requests: reqs.map(planned) };
|
|
7333
|
+
const results = [];
|
|
7334
|
+
for (const [i, req] of reqs.entries()) {
|
|
7335
|
+
try {
|
|
7336
|
+
const res = await call(req);
|
|
7337
|
+
results.push(withoutUndefined({ ok: true, status: res.status, id: idOf2(res.body, i) }));
|
|
7338
|
+
} catch (err) {
|
|
7339
|
+
const e = errorOf(err);
|
|
7340
|
+
if (e.code === "token_rejected" || e.code === "read_only_token" || e.code === "network") throw err;
|
|
7341
|
+
results.push(withoutUndefined({ ok: false, status: e.status, error: e.message, code: e.code }));
|
|
7342
|
+
}
|
|
7343
|
+
}
|
|
7344
|
+
return { dryRun: false, requested: reqs.length, applied: results.filter((r) => r["ok"] === true).length, results };
|
|
7345
|
+
}
|
|
7346
|
+
const fingerprint = (req) => JSON.stringify([req.method, req.path, req.query ?? {}, req.body ?? null]);
|
|
7347
|
+
const str2 = (v) => v;
|
|
7348
|
+
const handlers = {
|
|
7349
|
+
async plune_project_context() {
|
|
7350
|
+
const [projects, suites, cases, missions, queue] = await Promise.all([
|
|
7351
|
+
call({ method: "GET", path: "/v1/projects" }),
|
|
7352
|
+
call({ method: "GET", path: "/v1/suites" }),
|
|
7353
|
+
call({ method: "GET", path: "/v1/test-cases", query: { limit: "1", stability: "0" } }),
|
|
7354
|
+
call({ method: "GET", path: "/v1/missions" }),
|
|
7355
|
+
call({ method: "GET", path: "/v1/review-items", query: { state: "pending", limit: "100" } })
|
|
7356
|
+
]);
|
|
7357
|
+
const projectList = asArray(asRecord(projects.body)["projects"]);
|
|
7358
|
+
const only = projectList.length === 1 ? asRecord(projectList[0])["id"] : void 0;
|
|
7359
|
+
const projectId = opts.projectId ?? (typeof only === "string" ? only : null);
|
|
7360
|
+
const total = asRecord(cases.body)["total"];
|
|
7361
|
+
return {
|
|
7362
|
+
projectId,
|
|
7363
|
+
projects: projectList,
|
|
7364
|
+
suites: asArray(asRecord(suites.body)["suites"]),
|
|
7365
|
+
cases: { total: typeof total === "number" ? total : 0 },
|
|
7366
|
+
missions: asArray(asRecord(missions.body)["missions"]),
|
|
7367
|
+
reviewQueue: { pending: asArray(asRecord(queue.body)["items"]).length, more: cursorOf(asRecord(queue.body)["nextCursor"]) !== null }
|
|
7368
|
+
};
|
|
7369
|
+
},
|
|
7370
|
+
async plune_get(i) {
|
|
7371
|
+
const route = { "test-case": "/v1/test-cases/:id", run: "/v1/runs/:id", "review-item": "/v1/review-items/:id" }[str2(i["kind"])];
|
|
7372
|
+
const res = await call({ method: "GET", path: path19(route, str2(i["id"])) });
|
|
7373
|
+
return { kind: str2(i["kind"]), item: asRecord(res.body) };
|
|
7374
|
+
},
|
|
7375
|
+
async plune_cases_search(i) {
|
|
7376
|
+
const query = withoutUndefined({
|
|
7377
|
+
...i["filters"],
|
|
7378
|
+
limit: i["limit"] === void 0 ? void 0 : String(i["limit"]),
|
|
7379
|
+
cursor: i["cursor"],
|
|
7380
|
+
stability: "0"
|
|
7381
|
+
});
|
|
7382
|
+
const body = asRecord((await call({ method: "GET", path: "/v1/test-cases", query })).body);
|
|
7383
|
+
return withoutUndefined({
|
|
7384
|
+
testCases: asArray(body["testCases"]),
|
|
7385
|
+
total: typeof body["total"] === "number" ? body["total"] : void 0,
|
|
7386
|
+
matching: typeof body["matching"] === "number" ? body["matching"] : void 0,
|
|
7387
|
+
nextCursor: cursorOf(body["nextCursor"])
|
|
7388
|
+
});
|
|
7389
|
+
},
|
|
7390
|
+
async plune_review_list(i) {
|
|
7391
|
+
const kinds = i["kinds"];
|
|
7392
|
+
const query = withoutUndefined({
|
|
7393
|
+
state: i["state"],
|
|
7394
|
+
kind: kinds?.join(","),
|
|
7395
|
+
limit: i["limit"] === void 0 ? void 0 : String(i["limit"]),
|
|
7396
|
+
cursor: i["cursor"]
|
|
7397
|
+
});
|
|
7398
|
+
const body = asRecord((await call({ method: "GET", path: "/v1/review-items", query })).body);
|
|
7399
|
+
return { items: asArray(body["items"]), nextCursor: cursorOf(body["nextCursor"]) };
|
|
7400
|
+
},
|
|
7401
|
+
async plune_review_decide(i) {
|
|
7402
|
+
const itemIds = i["itemIds"];
|
|
7403
|
+
const res = await call({
|
|
7404
|
+
method: "POST",
|
|
7405
|
+
path: "/v1/review-items/decisions",
|
|
7406
|
+
...i["confirm"] === void 0 ? {} : { query: { confirm: str2(i["confirm"]) } },
|
|
7407
|
+
body: withoutUndefined({ itemIds, state: i["verdict"], rejectionCategory: i["rejectionCategory"], note: i["note"] })
|
|
7408
|
+
});
|
|
7409
|
+
const body = asRecord(res.body);
|
|
7410
|
+
if (body["preview"] !== void 0) return { requested: itemIds.length, applied: 0, preview: asRecord(body["preview"]) };
|
|
7411
|
+
const applied = body["applied"];
|
|
7412
|
+
return { requested: itemIds.length, applied: typeof applied === "number" ? applied : 0, report: body };
|
|
7413
|
+
},
|
|
7414
|
+
async plune_case_propose(i) {
|
|
7415
|
+
const cases = i["cases"].map((c) => {
|
|
7416
|
+
const steps = c["steps"];
|
|
7417
|
+
const stableId = (0, import_node_crypto5.createHash)("sha256").update([c["title"], ...steps, c["expected"]].join("\n")).digest("hex").slice(0, 32);
|
|
7418
|
+
return withoutUndefined({ stableId, execution: "manual", ...c });
|
|
7419
|
+
});
|
|
7420
|
+
proposals += 1;
|
|
7421
|
+
const req = {
|
|
7422
|
+
method: "POST",
|
|
7423
|
+
path: "/v1/ingest/cairn",
|
|
7424
|
+
body: { schemaVersion: 1, run: { runId: `mcp-${sessionId}-${proposals}`, mode: "design", target: str2(i["target"]) }, cases }
|
|
7425
|
+
};
|
|
7426
|
+
if (i["dry_run"] === true) return { dryRun: true, requested: cases.length, applied: 0, requests: [planned(req)] };
|
|
7427
|
+
const summary = asRecord((await call(req)).body);
|
|
7428
|
+
const proposed = summary["proposed"];
|
|
7429
|
+
return { dryRun: false, requested: cases.length, applied: typeof proposed === "number" ? proposed : 0, report: summary };
|
|
7430
|
+
},
|
|
7431
|
+
async plune_mission_create(i) {
|
|
7432
|
+
const { dry_run, ...body } = i;
|
|
7433
|
+
return write({ method: "POST", path: "/v1/missions", body: withoutUndefined(body) }, dry_run);
|
|
7434
|
+
},
|
|
7435
|
+
async plune_mission_get(i) {
|
|
7436
|
+
const res = await call({ method: "GET", path: path19("/v1/missions/:id", str2(i["missionId"])) });
|
|
7437
|
+
return { mission: asRecord(res.body) };
|
|
7438
|
+
},
|
|
7439
|
+
async plune_mission_update(i) {
|
|
7440
|
+
const { dry_run, missionId: missionId2, ...body } = i;
|
|
7441
|
+
return write({ method: "PATCH", path: path19("/v1/missions/:id", str2(missionId2)), body: withoutUndefined(body) }, dry_run);
|
|
7442
|
+
},
|
|
7443
|
+
async plune_source_add(i) {
|
|
7444
|
+
const parsed = (i["kind"] === "file" ? fileSource : linkSource).safeParse(withoutUndefined(i));
|
|
7445
|
+
if (!parsed.success) return fail("invalid_input", `The input does not fit: ${zodMessage(parsed.error)}`);
|
|
7446
|
+
const { dry_run, missionId: missionId2, kind, ...rest } = parsed.data;
|
|
7447
|
+
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 }) };
|
|
7448
|
+
return write(req, dry_run);
|
|
7449
|
+
},
|
|
7450
|
+
async plune_question_ask(i) {
|
|
7451
|
+
const { dry_run, ...body } = i;
|
|
7452
|
+
return write({ method: "POST", path: "/v1/review-items/questions", body: withoutUndefined(body) }, dry_run);
|
|
7453
|
+
},
|
|
7454
|
+
async plune_question_list(i) {
|
|
7455
|
+
const query = withoutUndefined({
|
|
7456
|
+
kind: "question",
|
|
7457
|
+
state: i["state"],
|
|
7458
|
+
limit: i["limit"] === void 0 ? void 0 : String(i["limit"]),
|
|
7459
|
+
cursor: i["cursor"]
|
|
7460
|
+
});
|
|
7461
|
+
const body = asRecord((await call({ method: "GET", path: "/v1/review-items", query })).body);
|
|
7462
|
+
const missionId2 = i["missionId"];
|
|
7463
|
+
const questions = asArray(body["items"]).filter(
|
|
7464
|
+
(item) => missionId2 === void 0 || asRecord(asRecord(item)["question"])["missionId"] === missionId2
|
|
7465
|
+
);
|
|
7466
|
+
return { questions, nextCursor: cursorOf(body["nextCursor"]) };
|
|
7467
|
+
},
|
|
7468
|
+
async plune_strategy_propose(i) {
|
|
7469
|
+
return write({ method: "POST", path: path19("/v1/missions/:id/strategy", str2(i["missionId"])), body: i["content"] }, i["dry_run"]);
|
|
7470
|
+
},
|
|
7471
|
+
async plune_report_request(i) {
|
|
7472
|
+
return write({ method: "POST", path: path19("/v1/missions/:id/report", str2(i["missionId"])), body: {} }, i["dry_run"]);
|
|
7473
|
+
},
|
|
7474
|
+
async plune_discover_tools(i) {
|
|
7475
|
+
const names = i["names"] ?? [];
|
|
7476
|
+
const unknown = names.filter((n) => !isPluneToolName(n));
|
|
7477
|
+
if (unknown.length > 0) return fail("invalid_input", `No such tool: ${unknown.join(", ")}. Call this tool without names to see what there is.`);
|
|
7478
|
+
const refused = names.filter((n) => scope === "read" && pluneTool(n).write);
|
|
7479
|
+
if (refused.length > 0) return fail("read_only_token", readScopeMessage(`${refused.join(", ")} cannot be switched on`));
|
|
7480
|
+
for (const n of names) revealed.add(n);
|
|
7481
|
+
const words = (i["query"] ?? "").toLowerCase().split(/\s+/).filter(Boolean);
|
|
7482
|
+
const visible = visibleTools();
|
|
7483
|
+
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 }));
|
|
7484
|
+
return { revealed: names, available, visible };
|
|
7485
|
+
},
|
|
7486
|
+
async plune_cases_update(i) {
|
|
7487
|
+
const cases = i["cases"];
|
|
7488
|
+
return batch(
|
|
7489
|
+
cases.map((c) => ({ method: "PATCH", path: path19("/v1/test-cases/:id", c.id), body: c.changes })),
|
|
7490
|
+
i["dry_run"],
|
|
7491
|
+
(_b, n) => cases[n]?.id
|
|
7492
|
+
);
|
|
7493
|
+
},
|
|
7494
|
+
async plune_run_create(i) {
|
|
7495
|
+
return write({ method: "POST", path: "/v1/runs", body: i["run"] }, i["dry_run"]);
|
|
7496
|
+
},
|
|
7497
|
+
async plune_results_write(i) {
|
|
7498
|
+
const results = i["results"];
|
|
7499
|
+
const req = {
|
|
7500
|
+
method: "POST",
|
|
7501
|
+
path: path19("/v1/runs/:id/results", str2(i["runId"])),
|
|
7502
|
+
body: withoutUndefined({ results, combination: i["combination"], configuration: i["configuration"] })
|
|
7503
|
+
};
|
|
7504
|
+
if (i["dry_run"] === true) return { dryRun: true, requested: results.length, applied: 0, requests: [planned(req)] };
|
|
7505
|
+
const report = asRecord((await call(req)).body);
|
|
7506
|
+
const accepted = asRecord(report["counts"])["accepted"];
|
|
7507
|
+
return { dryRun: false, requested: results.length, applied: typeof accepted === "number" ? accepted : 0, report };
|
|
7508
|
+
},
|
|
7509
|
+
async plune_endpoint_propose(i) {
|
|
7510
|
+
return batch(
|
|
7511
|
+
i["endpoints"].map((e) => ({ method: "POST", path: "/v1/review-items/endpoints", body: withoutUndefined(e) })),
|
|
7512
|
+
i["dry_run"],
|
|
7513
|
+
(b) => asRecord(b)["id"]
|
|
7514
|
+
);
|
|
7515
|
+
},
|
|
7516
|
+
async plune_api_request(i) {
|
|
7517
|
+
const method = str2(i["method"]);
|
|
7518
|
+
const p = safePath(opts.apiUrl, str2(i["path"]));
|
|
7519
|
+
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)`);
|
|
7520
|
+
const query = i["query"];
|
|
7521
|
+
const req = withoutUndefined({ method, path: p, query, body: i["body"] });
|
|
7522
|
+
if (i["dry_run"] === true) return { dryRun: true, request: planned(req) };
|
|
7523
|
+
const confirm2 = i["confirm"];
|
|
7524
|
+
if (isDeletion(req)) {
|
|
7525
|
+
const t = now();
|
|
7526
|
+
for (const [k, v] of confirms) if (v.expires <= t) confirms.delete(k);
|
|
7527
|
+
if (confirm2 === void 0) {
|
|
7528
|
+
const value2 = (0, import_node_crypto5.randomBytes)(24).toString("base64url");
|
|
7529
|
+
confirms.set(value2, { fingerprint: fingerprint(req), expires: t + CONFIRM_TTL_MS });
|
|
7530
|
+
return {
|
|
7531
|
+
preview: {
|
|
7532
|
+
action: "delete",
|
|
7533
|
+
method,
|
|
7534
|
+
path: p,
|
|
7535
|
+
confirm: value2,
|
|
7536
|
+
expiresInSeconds: CONFIRM_TTL_MS / 1e3,
|
|
7537
|
+
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."
|
|
7538
|
+
}
|
|
7539
|
+
};
|
|
7540
|
+
}
|
|
7541
|
+
const held = confirms.get(confirm2);
|
|
7542
|
+
confirms.delete(confirm2);
|
|
7543
|
+
if (held === void 0 || held.fingerprint !== fingerprint(req))
|
|
7544
|
+
return fail("confirm_invalid", "confirm is unknown, expired, used, or for another request \u2014 repeat the call without it to get a new preview");
|
|
7545
|
+
} else if (confirm2 !== void 0) {
|
|
7546
|
+
req.query = { ...query, confirm: confirm2 };
|
|
7547
|
+
}
|
|
7548
|
+
const res = await call(req);
|
|
7549
|
+
return { status: res.status, body: res.body };
|
|
7550
|
+
}
|
|
7551
|
+
};
|
|
7552
|
+
return {
|
|
7553
|
+
sessionId,
|
|
7554
|
+
scope: () => scope,
|
|
7555
|
+
visibleTools,
|
|
7556
|
+
async execute(name, input) {
|
|
7557
|
+
const handler = isPluneToolName(name) ? handlers[name] : void 0;
|
|
7558
|
+
if (handler === void 0) return fail("unknown_tool", `No such tool: ${name}.`);
|
|
7559
|
+
const tool = pluneTool(name);
|
|
7560
|
+
const parsed = tool.input.safeParse(input ?? {});
|
|
7561
|
+
if (!parsed.success) return fail("invalid_input", `The input does not fit: ${zodMessage(parsed.error)}`);
|
|
7562
|
+
const args = parsed.data;
|
|
7563
|
+
const writes = name === "plune_api_request" ? rawWrites(str2(args["method"]), str2(args["path"])) : tool.write;
|
|
7564
|
+
if (writes && scope === "read" && args["dry_run"] !== true) return fail("read_only_token", readScopeMessage(`${name} is refused before any request`));
|
|
7565
|
+
try {
|
|
7566
|
+
const out = await handler(args);
|
|
7567
|
+
return "ok" in out && typeof out["ok"] === "boolean" && ("error" in out || "data" in out) ? out : { ok: true, data: out };
|
|
7568
|
+
} catch (err) {
|
|
7569
|
+
return { ok: false, error: errorOf(err) };
|
|
7570
|
+
}
|
|
7571
|
+
}
|
|
7572
|
+
};
|
|
7573
|
+
}
|
|
7574
|
+
var import_node_crypto5, CONFIRM_TTL_MS, READ_ONLY_REFUSAL, readScopeMessage, fail, zodMessage, path19, withoutUndefined, asRecord, asArray, cursorOf, rawWrites;
|
|
7575
|
+
var init_execute = __esm({
|
|
7576
|
+
"src/mcp-tools/execute.ts"() {
|
|
7577
|
+
"use strict";
|
|
7578
|
+
init_cjs_shims();
|
|
7579
|
+
import_node_crypto5 = require("crypto");
|
|
7580
|
+
init_client2();
|
|
7581
|
+
init_contract();
|
|
7582
|
+
init_schemas();
|
|
7583
|
+
CONFIRM_TTL_MS = 5 * 60 * 1e3;
|
|
7584
|
+
READ_ONLY_REFUSAL = "read-only token";
|
|
7585
|
+
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.`;
|
|
7586
|
+
fail = (code, message, extra = {}) => ({
|
|
7587
|
+
ok: false,
|
|
7588
|
+
error: { code, message, ...extra }
|
|
7589
|
+
});
|
|
7590
|
+
zodMessage = (error) => error.issues.map((i) => `${i.path.join(".") || "(input)"}: ${i.message}`).join("; ");
|
|
7591
|
+
path19 = (template, ...ids) => {
|
|
7592
|
+
let i = 0;
|
|
7593
|
+
return template.replace(/:[a-z]+/gi, () => encodeURIComponent(ids[i++] ?? ""));
|
|
7594
|
+
};
|
|
7595
|
+
withoutUndefined = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
|
|
7596
|
+
asRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
7597
|
+
asArray = (v) => Array.isArray(v) ? v : [];
|
|
7598
|
+
cursorOf = (v) => typeof v === "string" ? v : null;
|
|
7599
|
+
rawWrites = (method, p) => method !== "GET" && !(method === "POST" && p === "/v1/test-cases/resolve");
|
|
7600
|
+
}
|
|
7601
|
+
});
|
|
7602
|
+
|
|
7603
|
+
// src/mcp-tools/index.ts
|
|
7604
|
+
var init_mcp_tools = __esm({
|
|
7605
|
+
"src/mcp-tools/index.ts"() {
|
|
7606
|
+
"use strict";
|
|
7607
|
+
init_cjs_shims();
|
|
7608
|
+
init_contract();
|
|
7609
|
+
init_execute();
|
|
7610
|
+
init_client2();
|
|
7611
|
+
init_schemas();
|
|
7612
|
+
}
|
|
7613
|
+
});
|
|
7614
|
+
|
|
7615
|
+
// src/mcp/server.ts
|
|
7616
|
+
var server_exports = {};
|
|
7617
|
+
__export(server_exports, {
|
|
7618
|
+
createPluneMcpServer: () => createPluneMcpServer,
|
|
7619
|
+
serveStdio: () => serveStdio
|
|
7620
|
+
});
|
|
7621
|
+
function toResult(outcome) {
|
|
7622
|
+
if (outcome.ok) {
|
|
7623
|
+
return { content: [{ type: "text", text: JSON.stringify(outcome.data, null, 2) }], structuredContent: outcome.data };
|
|
7624
|
+
}
|
|
7625
|
+
return {
|
|
7626
|
+
isError: true,
|
|
7627
|
+
content: [{ type: "text", text: outcome.error.message }],
|
|
7628
|
+
structuredContent: { error: outcome.error }
|
|
7629
|
+
};
|
|
7630
|
+
}
|
|
7631
|
+
function createPluneMcpServer(opts) {
|
|
7632
|
+
const { version, ...executorOptions } = opts;
|
|
7633
|
+
const executor = createPluneToolExecutor({ ...executorOptions, userAgent: executorOptions.userAgent ?? `plune-cli/${version} mcp` });
|
|
7634
|
+
const server = new import_mcp.McpServer({ name: "plune", version }, { capabilities: { tools: { listChanged: true } } });
|
|
7635
|
+
const registered = /* @__PURE__ */ new Map();
|
|
7636
|
+
const sync = () => {
|
|
7637
|
+
const visible = new Set(executor.visibleTools());
|
|
7638
|
+
for (const [name, tool] of registered) {
|
|
7639
|
+
if (visible.has(name) && !tool.enabled) tool.enable();
|
|
7640
|
+
else if (!visible.has(name) && tool.enabled) tool.disable();
|
|
7641
|
+
}
|
|
7642
|
+
};
|
|
7643
|
+
for (const tool of PLUNE_TOOLS) {
|
|
7644
|
+
if (opts.scope === "read" && tool.write) continue;
|
|
7645
|
+
const handle = server.registerTool(
|
|
7646
|
+
tool.name,
|
|
7647
|
+
{
|
|
7648
|
+
title: tool.title,
|
|
7649
|
+
description: tool.description,
|
|
7650
|
+
inputSchema: tool.input,
|
|
7651
|
+
outputSchema: tool.output,
|
|
7652
|
+
annotations: {
|
|
7653
|
+
title: tool.title,
|
|
7654
|
+
readOnlyHint: !tool.write,
|
|
7655
|
+
destructiveHint: tool.name === "plune_api_request",
|
|
7656
|
+
openWorldHint: false
|
|
7657
|
+
}
|
|
7658
|
+
},
|
|
7659
|
+
async (args) => {
|
|
7660
|
+
const outcome = await executor.execute(tool.name, args);
|
|
7661
|
+
sync();
|
|
7662
|
+
return toResult(outcome);
|
|
7663
|
+
}
|
|
7664
|
+
);
|
|
7665
|
+
registered.set(tool.name, handle);
|
|
7666
|
+
}
|
|
7667
|
+
sync();
|
|
7668
|
+
return server;
|
|
7669
|
+
}
|
|
7670
|
+
async function serveStdio(opts) {
|
|
7671
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
7672
|
+
const server = createPluneMcpServer(opts);
|
|
7673
|
+
const transport = new StdioServerTransport();
|
|
7674
|
+
const closed = new Promise((resolve11) => {
|
|
7675
|
+
transport.onclose = () => resolve11();
|
|
7676
|
+
});
|
|
7677
|
+
await server.connect(transport);
|
|
7678
|
+
await closed;
|
|
7679
|
+
}
|
|
7680
|
+
var import_mcp;
|
|
7681
|
+
var init_server = __esm({
|
|
7682
|
+
"src/mcp/server.ts"() {
|
|
7683
|
+
"use strict";
|
|
7684
|
+
init_cjs_shims();
|
|
7685
|
+
import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
7686
|
+
init_mcp_tools();
|
|
7687
|
+
}
|
|
7688
|
+
});
|
|
7689
|
+
|
|
7690
|
+
// src/cli/commands/mcp.ts
|
|
7691
|
+
var mcp_exports = {};
|
|
7692
|
+
__export(mcp_exports, {
|
|
7693
|
+
handleMcpServe: () => handleMcpServe
|
|
7694
|
+
});
|
|
7695
|
+
async function handleMcpServe(flags, deps) {
|
|
7696
|
+
const token = (deps.loadToken ?? loadToken)();
|
|
7697
|
+
if (token === null) throw new NotLoggedInError();
|
|
7698
|
+
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
7699
|
+
const projectId = flags.project?.trim() || process.env["PLUNE_PROJECT"]?.trim() || void 0;
|
|
7700
|
+
const serve = deps.serve ?? (await Promise.resolve().then(() => (init_server(), server_exports))).serveStdio;
|
|
7701
|
+
await serve({
|
|
7702
|
+
token,
|
|
7703
|
+
apiUrl,
|
|
7704
|
+
version: deps.version,
|
|
7705
|
+
...projectId !== void 0 ? { projectId } : {},
|
|
7706
|
+
...flags.readOnly === true ? { scope: "read" } : {}
|
|
7707
|
+
});
|
|
7708
|
+
}
|
|
7709
|
+
var init_mcp = __esm({
|
|
7710
|
+
"src/cli/commands/mcp.ts"() {
|
|
7711
|
+
"use strict";
|
|
7712
|
+
init_cjs_shims();
|
|
7713
|
+
init_credentials();
|
|
7714
|
+
init_api_url();
|
|
7715
|
+
init_sync();
|
|
7716
|
+
}
|
|
7717
|
+
});
|
|
7718
|
+
|
|
6420
7719
|
// src/cli/commands/cairn-artifact.ts
|
|
6421
7720
|
async function findLatestCairnRun(runsDir = "runs") {
|
|
6422
|
-
const entries = await
|
|
7721
|
+
const entries = await fs17.readdir(runsDir, { withFileTypes: true }).catch(() => null);
|
|
6423
7722
|
if (entries === null) throw new NoCairnRunsError(runsDir);
|
|
6424
7723
|
let newest;
|
|
6425
7724
|
for (const entry of entries) {
|
|
6426
7725
|
if (!entry.isDirectory()) continue;
|
|
6427
|
-
const dir =
|
|
7726
|
+
const dir = path20.join(runsDir, entry.name);
|
|
6428
7727
|
try {
|
|
6429
|
-
const report = await
|
|
7728
|
+
const report = await fs17.lstat(path20.join(dir, "report.json"));
|
|
6430
7729
|
if (!report.isFile()) continue;
|
|
6431
7730
|
if (newest === void 0 || report.mtimeMs > newest.at) {
|
|
6432
7731
|
newest = { dir, at: report.mtimeMs };
|
|
@@ -6439,16 +7738,16 @@ async function findLatestCairnRun(runsDir = "runs") {
|
|
|
6439
7738
|
}
|
|
6440
7739
|
async function readJson(file) {
|
|
6441
7740
|
try {
|
|
6442
|
-
return JSON.parse(await
|
|
7741
|
+
return JSON.parse(await fs17.readFile(file, "utf8"));
|
|
6443
7742
|
} catch {
|
|
6444
7743
|
return void 0;
|
|
6445
7744
|
}
|
|
6446
7745
|
}
|
|
6447
7746
|
async function defaultReadEvidence(dir) {
|
|
6448
|
-
const raw = await readJson(
|
|
7747
|
+
const raw = await readJson(path20.join(dir, "api-evidence.json"));
|
|
6449
7748
|
if (!Array.isArray(raw)) return void 0;
|
|
6450
7749
|
return raw.map((r) => {
|
|
6451
|
-
const rec =
|
|
7750
|
+
const rec = asRecord2(r);
|
|
6452
7751
|
const error = str(rec["error"]);
|
|
6453
7752
|
return { name: str(rec["name"]), passed: rec["passed"] === true, ...error !== "" ? { error } : {} };
|
|
6454
7753
|
});
|
|
@@ -6456,7 +7755,7 @@ async function defaultReadEvidence(dir) {
|
|
|
6456
7755
|
function casesFromDesign(report) {
|
|
6457
7756
|
const list2 = Array.isArray(report["testCases"]) ? report["testCases"] : [];
|
|
6458
7757
|
return list2.map((raw) => {
|
|
6459
|
-
const c =
|
|
7758
|
+
const c = asRecord2(raw);
|
|
6460
7759
|
const steps = (Array.isArray(c["steps"]) ? c["steps"] : []).map((s) => str(s)).filter((s) => s !== "");
|
|
6461
7760
|
return {
|
|
6462
7761
|
stableId: str(c["stableId"]),
|
|
@@ -6472,7 +7771,7 @@ function casesFromDesign(report) {
|
|
|
6472
7771
|
function casesFromApi(report, verdicts) {
|
|
6473
7772
|
const list2 = Array.isArray(report["cases"]) ? report["cases"] : [];
|
|
6474
7773
|
return list2.map((raw) => {
|
|
6475
|
-
const c =
|
|
7774
|
+
const c = asRecord2(raw);
|
|
6476
7775
|
const name = str(c["name"], "unnamed operation");
|
|
6477
7776
|
const request = `${str(c["method"], "GET")} ${str(c["path"], "/")}`;
|
|
6478
7777
|
const verdict = verdicts.get(name);
|
|
@@ -6488,9 +7787,9 @@ function casesFromApi(report, verdicts) {
|
|
|
6488
7787
|
});
|
|
6489
7788
|
}
|
|
6490
7789
|
async function readCairnRun(dir, deps = {}) {
|
|
6491
|
-
const raw = await readJson(
|
|
7790
|
+
const raw = await readJson(path20.join(dir, "report.json"));
|
|
6492
7791
|
if (raw === void 0) throw new CairnRunNotFoundError(dir);
|
|
6493
|
-
const report =
|
|
7792
|
+
const report = asRecord2(raw);
|
|
6494
7793
|
if (report["schemaVersion"] !== SUPPORTED_ARTIFACT_VERSION) {
|
|
6495
7794
|
throw new CairnRunVersionError(report["schemaVersion"]);
|
|
6496
7795
|
}
|
|
@@ -6518,20 +7817,20 @@ async function readCairnRun(dir, deps = {}) {
|
|
|
6518
7817
|
return {
|
|
6519
7818
|
schemaVersion: INGEST_SCHEMA_VERSION,
|
|
6520
7819
|
run: {
|
|
6521
|
-
runId: str(report["runId"],
|
|
7820
|
+
runId: str(report["runId"], path20.basename(dir)),
|
|
6522
7821
|
mode,
|
|
6523
7822
|
target: str(report["url"], "unknown")
|
|
6524
7823
|
},
|
|
6525
7824
|
cases
|
|
6526
7825
|
};
|
|
6527
7826
|
}
|
|
6528
|
-
var
|
|
7827
|
+
var fs17, path20, SUPPORTED_ARTIFACT_VERSION, INGEST_SCHEMA_VERSION, NoCairnRunsError, CairnRunNotFoundError, CairnRunVersionError, CairnRunPartialError, CairnEvidenceAmbiguousError, asRecord2, str;
|
|
6529
7828
|
var init_cairn_artifact = __esm({
|
|
6530
7829
|
"src/cli/commands/cairn-artifact.ts"() {
|
|
6531
7830
|
"use strict";
|
|
6532
7831
|
init_cjs_shims();
|
|
6533
|
-
|
|
6534
|
-
|
|
7832
|
+
fs17 = __toESM(require("fs/promises"), 1);
|
|
7833
|
+
path20 = __toESM(require("path"), 1);
|
|
6535
7834
|
SUPPORTED_ARTIFACT_VERSION = 1;
|
|
6536
7835
|
INGEST_SCHEMA_VERSION = 1;
|
|
6537
7836
|
NoCairnRunsError = class extends Error {
|
|
@@ -6570,7 +7869,7 @@ var init_cairn_artifact = __esm({
|
|
|
6570
7869
|
this.name = "CairnEvidenceAmbiguousError";
|
|
6571
7870
|
}
|
|
6572
7871
|
};
|
|
6573
|
-
|
|
7872
|
+
asRecord2 = (v) => typeof v === "object" && v !== null ? v : {};
|
|
6574
7873
|
str = (v, fallback = "") => typeof v === "string" && v !== "" ? v : fallback;
|
|
6575
7874
|
}
|
|
6576
7875
|
});
|
|
@@ -6603,7 +7902,7 @@ async function handleIngest(deps) {
|
|
|
6603
7902
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
6604
7903
|
const endpoint = `${apiUrl}/v1/ingest/cairn`;
|
|
6605
7904
|
const job = new JobReporter(doFetch, apiUrl, token, "ingest");
|
|
6606
|
-
await job.open(
|
|
7905
|
+
await job.open(path21.basename(path21.resolve(dir)), payload.cases.length);
|
|
6607
7906
|
try {
|
|
6608
7907
|
const result = await upload();
|
|
6609
7908
|
await job.uploaded({ subjectId: result.runId || void 0 });
|
|
@@ -6662,12 +7961,12 @@ function formatIngestResult(r) {
|
|
|
6662
7961
|
return `${lines.join("\n")}
|
|
6663
7962
|
`;
|
|
6664
7963
|
}
|
|
6665
|
-
var
|
|
7964
|
+
var path21, IngestNotLoggedInError, IngestTokenRejectedError, IngestNetworkError, IngestHttpError;
|
|
6666
7965
|
var init_ingest = __esm({
|
|
6667
7966
|
"src/cli/commands/ingest.ts"() {
|
|
6668
7967
|
"use strict";
|
|
6669
7968
|
init_cjs_shims();
|
|
6670
|
-
|
|
7969
|
+
path21 = __toESM(require("path"), 1);
|
|
6671
7970
|
init_api_url();
|
|
6672
7971
|
init_credentials();
|
|
6673
7972
|
init_cairn_artifact();
|
|
@@ -7102,10 +8401,10 @@ function testsOfListing(listing) {
|
|
|
7102
8401
|
const annotated = spec.tests?.flatMap((t) => t.annotations ?? []).find(
|
|
7103
8402
|
(a) => a.type === "PluneId" && a.description !== void 0 && a.description !== ""
|
|
7104
8403
|
)?.description;
|
|
7105
|
-
const
|
|
8404
|
+
const id2 = annotated ?? TOKEN4.exec(spec.title)?.[1];
|
|
7106
8405
|
const pathTitle = [...titles, spec.title].join("#");
|
|
7107
8406
|
if (!found.has(pathTitle))
|
|
7108
|
-
found.set(pathTitle, { pathTitle, ...
|
|
8407
|
+
found.set(pathTitle, { pathTitle, ...id2 === void 0 ? {} : { id: id2 } });
|
|
7109
8408
|
}
|
|
7110
8409
|
for (const child of suite.suites ?? []) walk2(child, [...titles, child.title]);
|
|
7111
8410
|
};
|
|
@@ -7346,6 +8645,7 @@ function parseDuration(text2) {
|
|
|
7346
8645
|
}
|
|
7347
8646
|
|
|
7348
8647
|
// src/cli.ts
|
|
8648
|
+
init_min_score();
|
|
7349
8649
|
var __dirname = (0, import_node_path6.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
7350
8650
|
var pkg = JSON.parse((0, import_node_fs10.readFileSync)((0, import_node_path6.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
7351
8651
|
function toFormat(s) {
|
|
@@ -7541,10 +8841,10 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7541
8841
|
}
|
|
7542
8842
|
});
|
|
7543
8843
|
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(
|
|
7544
|
-
async (
|
|
8844
|
+
async (id2, options, command) => {
|
|
7545
8845
|
try {
|
|
7546
8846
|
const { handleRunFinish: handleRunFinish2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
7547
|
-
const code = await handleRunFinish2(
|
|
8847
|
+
const code = await handleRunFinish2(id2, {
|
|
7548
8848
|
terminate: options.terminate,
|
|
7549
8849
|
...options.reason !== void 0 ? { reason: options.reason } : {},
|
|
7550
8850
|
...options.failOnFailures ? { failOnFailures: true } : {}
|
|
@@ -7573,10 +8873,10 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7573
8873
|
runCommand.command("delete").argument(
|
|
7574
8874
|
"<id>",
|
|
7575
8875
|
"The run id \u2014 the one `plune run import` printed, or the one in the dashboard URL"
|
|
7576
|
-
).description("Delete a run and everything it produced \u2014 recoverable for 30 days").action(async (
|
|
8876
|
+
).description("Delete a run and everything it produced \u2014 recoverable for 30 days").action(async (id2, _options, command) => {
|
|
7577
8877
|
try {
|
|
7578
8878
|
const { handleRunDelete: handleRunDelete2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
7579
|
-
await handleRunDelete2(
|
|
8879
|
+
await handleRunDelete2(id2);
|
|
7580
8880
|
} catch (err) {
|
|
7581
8881
|
await failRunCommand(err, verboseOf(command));
|
|
7582
8882
|
}
|
|
@@ -7617,10 +8917,20 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7617
8917
|
});
|
|
7618
8918
|
runCommand.command("import").argument(
|
|
7619
8919
|
"<file>",
|
|
7620
|
-
"A JUnit XML or Playwright JSON report, a folder of JUnit XML files (target/surefire-reports, build/test-results/test), or an Allure allure-results folder"
|
|
8920
|
+
"A JUnit XML or Playwright JSON report, a k6 summary or Lighthouse JSON report, a folder of JUnit XML files (target/surefire-reports, build/test-results/test), or an Allure allure-results folder"
|
|
7621
8921
|
).description("Turn a report another runner wrote into a run in Plune \u2014 no provider key needed").option(
|
|
7622
8922
|
"--format <fmt>",
|
|
7623
|
-
"junit | playwright-json | allure; detected when omitted (a folder with a *-result.json is allure, any other folder is its *.xml files as junit)"
|
|
8923
|
+
"junit | playwright-json | allure | k6 | lighthouse; detected when omitted (a folder with a *-result.json is allure, any other folder is its *.xml files as junit)"
|
|
8924
|
+
).option(
|
|
8925
|
+
"--min-score <category=score>",
|
|
8926
|
+
"Lighthouse: the score (0-100) a category needs to pass, e.g. performance=90; repeat it per category. A category not named needs 90",
|
|
8927
|
+
(v, previous) => {
|
|
8928
|
+
try {
|
|
8929
|
+
return parseMinScore(v, previous);
|
|
8930
|
+
} catch (err) {
|
|
8931
|
+
throw new import_commander.InvalidArgumentError(err.message);
|
|
8932
|
+
}
|
|
8933
|
+
}
|
|
7624
8934
|
).option("--key <externalKey>", "The key several jobs share so their reports land in one run").option("--name <text>", "What to call the run in the list (default: the folder name, the time and where it ran; PLUNE_RUN_TITLE sets it too)").option("--create", "Offer tests nothing matched to the review queue", false).option(
|
|
7625
8935
|
"--fail-on-missing-attachments",
|
|
7626
8936
|
"Exit 1 when a file the report names ([[ATTACHMENT|path]] in JUnit, a source in allure-results) is not there; the results are sent either way",
|
|
@@ -7664,6 +8974,7 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7664
8974
|
...format !== void 0 ? { format } : {},
|
|
7665
8975
|
...options.key !== void 0 ? { key: options.key } : {},
|
|
7666
8976
|
...options.name !== void 0 ? { name: options.name } : {},
|
|
8977
|
+
...options.minScore !== void 0 ? { minScores: options.minScore } : {},
|
|
7667
8978
|
...options.create ? { create: true } : {},
|
|
7668
8979
|
...options.uploadConcurrency !== void 0 ? { uploadConcurrency: options.uploadConcurrency } : {},
|
|
7669
8980
|
...options.timeout !== void 0 ? { timeoutSec: options.timeout } : {},
|
|
@@ -7751,10 +9062,10 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7751
9062
|
const { handleLogin: handleLogin2, reportLoginFailure: reportLoginFailure2 } = await Promise.resolve().then(() => (init_login(), login_exports));
|
|
7752
9063
|
try {
|
|
7753
9064
|
const token = options.token ?? await readTokenFromStdin();
|
|
7754
|
-
const { path:
|
|
9065
|
+
const { path: path22, verified } = await handleLogin2({ token, skipVerify: options.skipVerify });
|
|
7755
9066
|
process.stdout.write(
|
|
7756
|
-
verified ? `Logged in. Token checked against the API and saved to ${
|
|
7757
|
-
` : `Token saved to ${
|
|
9067
|
+
verified ? `Logged in. Token checked against the API and saved to ${path22}
|
|
9068
|
+
` : `Token saved to ${path22} WITHOUT being checked (--skip-verify).
|
|
7758
9069
|
`
|
|
7759
9070
|
);
|
|
7760
9071
|
} catch (err) {
|
|
@@ -7771,27 +9082,46 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7771
9082
|
const verbose = command.optsWithGlobals().verbose === true;
|
|
7772
9083
|
const { handleLogout: handleLogout2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
|
|
7773
9084
|
try {
|
|
7774
|
-
const { removed, path:
|
|
9085
|
+
const { removed, path: path22 } = handleLogout2();
|
|
7775
9086
|
process.stdout.write(
|
|
7776
|
-
removed ? `Logged out. Removed ${
|
|
9087
|
+
removed ? `Logged out. Removed ${path22}
|
|
7777
9088
|
` : "Not logged in \u2014 nothing to remove.\n"
|
|
7778
9089
|
);
|
|
7779
9090
|
} catch (err) {
|
|
7780
9091
|
failUnexpected(err, verbose);
|
|
7781
9092
|
}
|
|
7782
9093
|
});
|
|
9094
|
+
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) => {
|
|
9095
|
+
const verbose = command.optsWithGlobals().verbose === true;
|
|
9096
|
+
const { handleMcpServe: handleMcpServe2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
|
|
9097
|
+
const { NotLoggedInError: NotLoggedInError2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
9098
|
+
try {
|
|
9099
|
+
await handleMcpServe2(
|
|
9100
|
+
{ project: options.project, readOnly: options.readOnly },
|
|
9101
|
+
{ version: pkg.version }
|
|
9102
|
+
);
|
|
9103
|
+
} catch (err) {
|
|
9104
|
+
if (err instanceof NotLoggedInError2) {
|
|
9105
|
+
process.stderr.write(`plune: ${err.message}
|
|
9106
|
+
`);
|
|
9107
|
+
process.exitCode = 2;
|
|
9108
|
+
return;
|
|
9109
|
+
}
|
|
9110
|
+
failUnexpected(err, verbose);
|
|
9111
|
+
}
|
|
9112
|
+
});
|
|
7783
9113
|
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) => {
|
|
7784
9114
|
const globals = command.optsWithGlobals();
|
|
7785
9115
|
const verbose = globals.verbose === true;
|
|
7786
9116
|
const { handleSync: handleSync2, reportSyncFailure: reportSyncFailure2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
7787
9117
|
const cwd = globals.config !== void 0 ? (0, import_node_path6.dirname)((0, import_node_path6.resolve)(globals.config)) : process.cwd();
|
|
7788
9118
|
try {
|
|
7789
|
-
const { id, url, gate, jobProblem } = await handleSync2({
|
|
9119
|
+
const { id: id2, url, gate, jobProblem } = await handleSync2({
|
|
7790
9120
|
cwd,
|
|
7791
9121
|
...options.file !== void 0 ? { file: options.file } : {},
|
|
7792
9122
|
...options.failOnFailures ? { failOnFailures: true } : {}
|
|
7793
9123
|
});
|
|
7794
|
-
process.stdout.write(`Synced run ${
|
|
9124
|
+
process.stdout.write(`Synced run ${id2} \u2192 ${url}
|
|
7795
9125
|
`);
|
|
7796
9126
|
if (jobProblem !== void 0) {
|
|
7797
9127
|
process.stderr.write(`plune: could not report this sync to the job queue (${jobProblem}) \u2014 the run is uploaded.
|
|
@@ -7934,12 +9264,12 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7934
9264
|
const planCommand = program.command("plan").description("Test plans on the platform \u2014 what a plan collects, as something a runner can take");
|
|
7935
9265
|
planCommand.command("grep").argument("<id>", "The plan").description(
|
|
7936
9266
|
"Print the plan's cases as one --grep pattern for the runner (GET /v1/plans/:id/cases)"
|
|
7937
|
-
).action(async (
|
|
9267
|
+
).action(async (id2, _options, command) => {
|
|
7938
9268
|
const verbose = command.optsWithGlobals().verbose === true;
|
|
7939
9269
|
const { handlePlanGrep: handlePlanGrep2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
|
|
7940
9270
|
const { reportCasesFailure: reportCasesFailure2 } = await Promise.resolve().then(() => (init_cases(), cases_exports));
|
|
7941
9271
|
try {
|
|
7942
|
-
const out = await handlePlanGrep2({ planId:
|
|
9272
|
+
const out = await handlePlanGrep2({ planId: id2 });
|
|
7943
9273
|
process.stdout.write(out.pattern + "\n");
|
|
7944
9274
|
process.stderr.write(
|
|
7945
9275
|
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.
|