@plune-ai/cli 0.34.1 → 0.34.3
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 +14 -2
- package/dist/cli.cjs +108 -34
- package/dist/index.cjs +25 -1
- package/dist/index.js +25 -1
- package/dist/mcp-tools.cjs +52 -3
- package/dist/mcp-tools.d.cts +47 -0
- package/dist/mcp-tools.d.ts +47 -0
- package/dist/mcp-tools.js +52 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,8 +70,10 @@ evals:
|
|
|
70
70
|
value: "Paris"
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
Datasets are JSONL, one row per line, shaped `{ "vars": { ... }, "expected"?: "..." }`.
|
|
74
|
-
|
|
73
|
+
Datasets are JSONL, one row per line, shaped `{ "vars": { ... }, "expected"?: "..." }`. A line that
|
|
74
|
+
is not valid JSON or not a row (no `vars`, or `vars` that is not an object) stops the run before
|
|
75
|
+
any model call with `<file>:<line>` and what is wrong, exit code `2`, like any other configuration
|
|
76
|
+
error. The provider API key is read from the environment based on `provider.type`:
|
|
75
77
|
|
|
76
78
|
| Provider | `provider.type` | Environment variable |
|
|
77
79
|
| ---------- | --------------- | -------------------- |
|
|
@@ -631,6 +633,16 @@ once; the rest it finds with `plune_discover_tools`. Every write tool takes `dry
|
|
|
631
633
|
100 items, and the agent is held to the platform's agent rules: a proposed case lands in the review
|
|
632
634
|
queue, a bulk decision needs a confirm, and nothing is deleted through MCP.
|
|
633
635
|
|
|
636
|
+
`plune_cases_generate` asks the project's AI for case proposals from pasted text (a requirement, a
|
|
637
|
+
story, a ticket's text; 20 words to 12 000 characters, up to 12 proposals, 8 by default). It spends the
|
|
638
|
+
project's AI budget, and its proposals wait in the review queue like any other until a person approves
|
|
639
|
+
them. The run is queued: the tool answers with its id, and `plune_cases_generation_get` reads where it
|
|
640
|
+
stands, what it produced and what it cost. A Jira ticket by its key is not taken over MCP: paste its
|
|
641
|
+
text. The platform's refusals (409 `duplicate_source` until `confirmDuplicates: true`, 422 for text
|
|
642
|
+
too short or too long, 429 limits, 503 / 409 when the project's AI is not set up) come back with
|
|
643
|
+
their `code` as the platform sent them. Both tools work in the server's `--project` unless the call
|
|
644
|
+
names a `projectId`.
|
|
645
|
+
|
|
634
646
|
Claude Code:
|
|
635
647
|
|
|
636
648
|
```bash
|
package/dist/cli.cjs
CHANGED
|
@@ -2096,7 +2096,31 @@ function loadDataset(ref, baseDir) {
|
|
|
2096
2096
|
return ref.examples;
|
|
2097
2097
|
}
|
|
2098
2098
|
const content = fs4.readFileSync(path4.resolve(baseDir, ref), "utf8");
|
|
2099
|
-
|
|
2099
|
+
const rows = [];
|
|
2100
|
+
content.split(/\r?\n/).forEach((line, i) => {
|
|
2101
|
+
if (line.trim().length === 0) return;
|
|
2102
|
+
let row;
|
|
2103
|
+
try {
|
|
2104
|
+
row = JSON.parse(line.trim());
|
|
2105
|
+
} catch (err) {
|
|
2106
|
+
throw new RunConfigError(`${ref}:${i + 1}: not valid JSON (${err.message})`);
|
|
2107
|
+
}
|
|
2108
|
+
const vars = isObject(row) ? row["vars"] : void 0;
|
|
2109
|
+
if (!isObject(vars)) {
|
|
2110
|
+
const why = isObject(row) && "vars" in row ? `vars: Invalid input: expected record, received ${typeName(vars)}` : 'this row has no "vars"';
|
|
2111
|
+
throw new RunConfigError(
|
|
2112
|
+
`${ref}:${i + 1}: a dataset row must be { "vars": { ... } }; ${why}`
|
|
2113
|
+
);
|
|
2114
|
+
}
|
|
2115
|
+
rows.push(row);
|
|
2116
|
+
});
|
|
2117
|
+
return rows;
|
|
2118
|
+
}
|
|
2119
|
+
function isObject(value2) {
|
|
2120
|
+
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
2121
|
+
}
|
|
2122
|
+
function typeName(value2) {
|
|
2123
|
+
return value2 === null ? "null" : Array.isArray(value2) ? "array" : typeof value2;
|
|
2100
2124
|
}
|
|
2101
2125
|
var fs4, path4;
|
|
2102
2126
|
var init_dataset = __esm({
|
|
@@ -2105,6 +2129,7 @@ var init_dataset = __esm({
|
|
|
2105
2129
|
init_cjs_shims();
|
|
2106
2130
|
fs4 = __toESM(require("fs"), 1);
|
|
2107
2131
|
path4 = __toESM(require("path"), 1);
|
|
2132
|
+
init_run();
|
|
2108
2133
|
}
|
|
2109
2134
|
});
|
|
2110
2135
|
|
|
@@ -5662,21 +5687,21 @@ var init_playwright_json = __esm({
|
|
|
5662
5687
|
});
|
|
5663
5688
|
|
|
5664
5689
|
// src/importers/k6.ts
|
|
5665
|
-
function
|
|
5690
|
+
function isObject2(value2) {
|
|
5666
5691
|
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
5667
5692
|
}
|
|
5668
5693
|
function isK6Summary(parsed) {
|
|
5669
|
-
if (!
|
|
5670
|
-
if (
|
|
5671
|
-
return Object.values(parsed["metrics"]).some((m) =>
|
|
5694
|
+
if (!isObject2(parsed) || !isObject2(parsed["metrics"])) return false;
|
|
5695
|
+
if (isObject2(parsed["root_group"])) return true;
|
|
5696
|
+
return Object.values(parsed["metrics"]).some((m) => isObject2(m) && (typeof m["type"] === "string" || isObject2(m["thresholds"])));
|
|
5672
5697
|
}
|
|
5673
5698
|
function passedOf(verdict) {
|
|
5674
|
-
if (
|
|
5699
|
+
if (isObject2(verdict) && typeof verdict["ok"] === "boolean") return verdict["ok"];
|
|
5675
5700
|
if (typeof verdict === "boolean") return !verdict;
|
|
5676
5701
|
return void 0;
|
|
5677
5702
|
}
|
|
5678
5703
|
function measuredOf(metric) {
|
|
5679
|
-
const values =
|
|
5704
|
+
const values = isObject2(metric.values) ? metric.values : metric;
|
|
5680
5705
|
const unit = metric.contains === "time" ? " ms" : "";
|
|
5681
5706
|
return Object.entries(values).filter(([name, v]) => name !== "thresholds" && typeof v === "number").map(([name, v]) => `${name}=${Math.round(v * 100) / 100}${unit}`).join(", ");
|
|
5682
5707
|
}
|
|
@@ -5687,13 +5712,13 @@ function readK6(source, file) {
|
|
|
5687
5712
|
} catch (err) {
|
|
5688
5713
|
throw new JsonReportError(file, `not JSON \u2014 ${printable(err instanceof Error ? err.message : String(err))}`);
|
|
5689
5714
|
}
|
|
5690
|
-
if (!
|
|
5715
|
+
if (!isObject2(parsed) || !isObject2(parsed["metrics"])) {
|
|
5691
5716
|
throw new JsonReportError(file, 'no "metrics" object \u2014 is this a k6 summary (k6 run --summary-export, or the data handleSummary receives)?');
|
|
5692
5717
|
}
|
|
5693
5718
|
const specRef = path13.basename(file);
|
|
5694
5719
|
const out = [];
|
|
5695
5720
|
for (const [name, raw] of Object.entries(parsed["metrics"])) {
|
|
5696
|
-
if (!
|
|
5721
|
+
if (!isObject2(raw) || !isObject2(raw["thresholds"])) continue;
|
|
5697
5722
|
const metric = raw;
|
|
5698
5723
|
const measured = measuredOf(metric);
|
|
5699
5724
|
for (const [expression, verdict] of Object.entries(metric.thresholds)) {
|
|
@@ -5734,11 +5759,11 @@ var init_k6 = __esm({
|
|
|
5734
5759
|
});
|
|
5735
5760
|
|
|
5736
5761
|
// src/importers/lighthouse.ts
|
|
5737
|
-
function
|
|
5762
|
+
function isObject3(value2) {
|
|
5738
5763
|
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
5739
5764
|
}
|
|
5740
5765
|
function isLighthouseReport(parsed) {
|
|
5741
|
-
return
|
|
5766
|
+
return isObject3(parsed) && typeof parsed["lighthouseVersion"] === "string" && isObject3(parsed["categories"]);
|
|
5742
5767
|
}
|
|
5743
5768
|
function idOf(key, category) {
|
|
5744
5769
|
return typeof category.id === "string" && category.id !== "" ? category.id : key;
|
|
@@ -5747,7 +5772,7 @@ function executionOf2(report) {
|
|
|
5747
5772
|
const started = typeof report["fetchTime"] === "string" ? new Date(report["fetchTime"]) : void 0;
|
|
5748
5773
|
if (started === void 0 || Number.isNaN(started.getTime())) return void 0;
|
|
5749
5774
|
const timing = report["timing"];
|
|
5750
|
-
const total =
|
|
5775
|
+
const total = isObject3(timing) && typeof timing["total"] === "number" && timing["total"] >= 0 ? Math.round(timing["total"]) : 0;
|
|
5751
5776
|
return {
|
|
5752
5777
|
startedAt: started.toISOString(),
|
|
5753
5778
|
finishedAt: new Date(started.getTime() + total).toISOString(),
|
|
@@ -5757,7 +5782,7 @@ function executionOf2(report) {
|
|
|
5757
5782
|
}
|
|
5758
5783
|
function runtimeErrorOf(report) {
|
|
5759
5784
|
const err = report["runtimeError"];
|
|
5760
|
-
if (!
|
|
5785
|
+
if (!isObject3(err)) return void 0;
|
|
5761
5786
|
const code = typeof err["code"] === "string" ? err["code"] : void 0;
|
|
5762
5787
|
if (code === "NO_ERROR") return void 0;
|
|
5763
5788
|
const message = typeof err["message"] === "string" ? err["message"] : void 0;
|
|
@@ -5778,11 +5803,11 @@ function readLighthouse(source, file, minScores = {}) {
|
|
|
5778
5803
|
const categories = report["categories"];
|
|
5779
5804
|
const url = ["requestedUrl", "finalDisplayedUrl", "finalUrl"].map((k) => report[k]).find((u) => typeof u === "string" && u !== "");
|
|
5780
5805
|
const settings = report["configSettings"];
|
|
5781
|
-
const formFactor =
|
|
5806
|
+
const formFactor = isObject3(settings) && typeof settings["formFactor"] === "string" && settings["formFactor"] !== "" ? settings["formFactor"] : void 0;
|
|
5782
5807
|
const runtimeError = runtimeErrorOf(report);
|
|
5783
5808
|
const execution = executionOf2(report);
|
|
5784
5809
|
const specRef = path14.basename(file);
|
|
5785
|
-
const known = Object.entries(categories).filter(([, raw]) =>
|
|
5810
|
+
const known = Object.entries(categories).filter(([, raw]) => isObject3(raw)).map(([key, raw]) => idOf(key, raw));
|
|
5786
5811
|
if (known.length > 0) {
|
|
5787
5812
|
for (const [id2, score] of Object.entries(minScores)) {
|
|
5788
5813
|
if (!known.includes(id2)) {
|
|
@@ -5809,7 +5834,7 @@ function readLighthouse(source, file, minScores = {}) {
|
|
|
5809
5834
|
});
|
|
5810
5835
|
};
|
|
5811
5836
|
for (const [key, raw] of Object.entries(categories)) {
|
|
5812
|
-
if (!
|
|
5837
|
+
if (!isObject3(raw)) continue;
|
|
5813
5838
|
const category = raw;
|
|
5814
5839
|
const id2 = idOf(key, category);
|
|
5815
5840
|
const name = typeof category.title === "string" && category.title !== "" ? category.title : id2;
|
|
@@ -6698,7 +6723,7 @@ var init_logout = __esm({
|
|
|
6698
6723
|
});
|
|
6699
6724
|
|
|
6700
6725
|
// src/mcp-tools/schemas.ts
|
|
6701
|
-
var import_zod4, BATCH_MAX2, isHttp, httpLink, id, dryRun, MISSION_STATES, MISSION_DOCUMENTATION, MISSION_LINK_KINDS, missionName, missionSummary, missionUrl, missionFields, sourceTitle, sourceVersion, linkSource, fileSource, ACCESS_KINDS, questionText, questionInput, STRATEGY_SOURCE_KINDS, TEST_LEVELS, QUALITY_CHARACTERISTICS, TEST_TECHNIQUES, RISK_SCALE, itemFields, strategyItem, items, unique, linkIds, share, strategyContent, SOURCE_REF_KINDS, proposedCase, API_METHODS, proposedEndpoint, REVIEW_STATES, REVIEW_KINDS, REJECTION_CATEGORIES, entity, plannedRequest, writeOutput, batchOutput, bulkPreview;
|
|
6726
|
+
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, CASE_GENERATION_TITLE_MAX, CASE_GENERATION_MAX_CASES, CASE_GENERATION_DEFAULT_CASES, CASE_GENERATION_MIN_WORDS, CASE_GENERATION_TEXT_MAX, caseGenerationBody, API_METHODS, proposedEndpoint, REVIEW_STATES, REVIEW_KINDS, REJECTION_CATEGORIES, entity, plannedRequest, writeOutput, batchOutput, bulkPreview;
|
|
6702
6727
|
var init_schemas = __esm({
|
|
6703
6728
|
"src/mcp-tools/schemas.ts"() {
|
|
6704
6729
|
"use strict";
|
|
@@ -6828,6 +6853,21 @@ var init_schemas = __esm({
|
|
|
6828
6853
|
source: import_zod4.z.strictObject({ kind: import_zod4.z.enum(SOURCE_REF_KINDS), value: import_zod4.z.string().min(1).max(100) }).optional().describe("What you read to design it"),
|
|
6829
6854
|
rationale: import_zod4.z.string().min(1).max(400).optional().describe("Why the case is worth having")
|
|
6830
6855
|
});
|
|
6856
|
+
CASE_GENERATION_TITLE_MAX = 300;
|
|
6857
|
+
CASE_GENERATION_MAX_CASES = 12;
|
|
6858
|
+
CASE_GENERATION_DEFAULT_CASES = 8;
|
|
6859
|
+
CASE_GENERATION_MIN_WORDS = 20;
|
|
6860
|
+
CASE_GENERATION_TEXT_MAX = 12e3;
|
|
6861
|
+
caseGenerationBody = {
|
|
6862
|
+
input: import_zod4.z.strictObject({
|
|
6863
|
+
kind: import_zod4.z.literal("pasted-text"),
|
|
6864
|
+
text: import_zod4.z.string().describe(`The requirement, story or ticket text, at least ${CASE_GENERATION_MIN_WORDS} words and at most ${CASE_GENERATION_TEXT_MAX} characters`),
|
|
6865
|
+
title: import_zod4.z.string().trim().max(CASE_GENERATION_TITLE_MAX).optional()
|
|
6866
|
+
}),
|
|
6867
|
+
maxCases: import_zod4.z.number().int().min(1).max(CASE_GENERATION_MAX_CASES).optional().describe(`How many proposals at most; ${CASE_GENERATION_DEFAULT_CASES} when left out`),
|
|
6868
|
+
language: import_zod4.z.string().trim().min(2).max(35).optional().describe("The cases' language; the project's AI language when left out"),
|
|
6869
|
+
confirmDuplicates: import_zod4.z.boolean().optional().describe("true: go ahead although cases or proposals from this text exist (the platform answers 409 duplicate_source first)")
|
|
6870
|
+
};
|
|
6831
6871
|
API_METHODS = ["GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"];
|
|
6832
6872
|
proposedEndpoint = import_zod4.z.strictObject({
|
|
6833
6873
|
method: import_zod4.z.enum(API_METHODS),
|
|
@@ -6890,7 +6930,7 @@ function isPluneToolName(name) {
|
|
|
6890
6930
|
function defaultVisibleTools(scope) {
|
|
6891
6931
|
return PLUNE_TOOLS.filter((t) => t.visibility === "core" && !(scope === "read" && t.write)).map((t) => t.name);
|
|
6892
6932
|
}
|
|
6893
|
-
var import_zod5, MAX_VISIBLE_TOOLS, define, listPage, missionId, PLUNE_TOOLS, PLUNE_TOOL_NAMES, BY_NAME;
|
|
6933
|
+
var import_zod5, MAX_VISIBLE_TOOLS, define, listPage, missionId, projectId, PLUNE_TOOLS, PLUNE_TOOL_NAMES, BY_NAME;
|
|
6894
6934
|
var init_contract = __esm({
|
|
6895
6935
|
"src/mcp-tools/contract.ts"() {
|
|
6896
6936
|
"use strict";
|
|
@@ -6904,6 +6944,7 @@ var init_contract = __esm({
|
|
|
6904
6944
|
cursor: import_zod5.z.string().min(1).optional().describe("nextCursor of the previous page")
|
|
6905
6945
|
};
|
|
6906
6946
|
missionId = id("The mission id");
|
|
6947
|
+
projectId = import_zod5.z.string().min(1).optional().describe("The project; the one the server was started with when left out");
|
|
6907
6948
|
PLUNE_TOOLS = [
|
|
6908
6949
|
define({
|
|
6909
6950
|
name: "plune_project_context",
|
|
@@ -6967,7 +7008,7 @@ var init_contract = __esm({
|
|
|
6967
7008
|
define({
|
|
6968
7009
|
name: "plune_review_decide",
|
|
6969
7010
|
title: "Decide queue entries",
|
|
6970
|
-
description: "Approve or reject up to 100 review queue entries in one decision. The first call changes nothing and returns a preview with a confirm value; repeat the same call with confirm to apply it.",
|
|
7011
|
+
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. An entry skipped as policy_refused was refused by the review policy of the project (for example, the person who proposed it cannot approve it): do not retry it \u2014 it needs another reviewer; its detail says why.",
|
|
6971
7012
|
input: import_zod5.z.strictObject({
|
|
6972
7013
|
itemIds: import_zod5.z.array(import_zod5.z.string().min(1)).min(1).max(BATCH_MAX2).refine((a) => new Set(a).size === a.length, { message: "an id is named once" }),
|
|
6973
7014
|
verdict: import_zod5.z.enum(["approved", "rejected"]),
|
|
@@ -6999,6 +7040,26 @@ var init_contract = __esm({
|
|
|
6999
7040
|
visibility: "core",
|
|
7000
7041
|
endpoints: ["POST /v1/ingest/cairn"]
|
|
7001
7042
|
}),
|
|
7043
|
+
define({
|
|
7044
|
+
name: "plune_cases_generate",
|
|
7045
|
+
title: "Generate cases from pasted text",
|
|
7046
|
+
description: "Generate test case proposals from pasted text \u2014 a requirement, a user story, the text of a ticket \u2014 with the project's AI. It spends the project's AI budget. The proposals land in the review queue and nothing becomes a case until a person approves it. The run is queued: keep its id and poll it with plune_cases_generation_get until it is done. A Jira ticket by its key is not taken here: paste its text.",
|
|
7047
|
+
input: import_zod5.z.strictObject({ projectId, ...caseGenerationBody, dry_run: dryRun }),
|
|
7048
|
+
output: writeOutput,
|
|
7049
|
+
write: true,
|
|
7050
|
+
visibility: "core",
|
|
7051
|
+
endpoints: ["POST /v1/projects/:id/case-generations"]
|
|
7052
|
+
}),
|
|
7053
|
+
define({
|
|
7054
|
+
name: "plune_cases_generation_get",
|
|
7055
|
+
title: "Read a case generation",
|
|
7056
|
+
description: "Check where a run of cases from pasted text stands \u2014 queued, running, done, failed or cancelled \u2014 with how many proposals it made, how many it skipped, and what it cost.",
|
|
7057
|
+
input: import_zod5.z.strictObject({ projectId, generationId: id("The id the start answered with") }),
|
|
7058
|
+
output: import_zod5.z.object({ generation: entity }),
|
|
7059
|
+
write: false,
|
|
7060
|
+
visibility: "core",
|
|
7061
|
+
endpoints: ["GET /v1/projects/:id/case-generations/:generationId"]
|
|
7062
|
+
}),
|
|
7002
7063
|
define({
|
|
7003
7064
|
name: "plune_mission_create",
|
|
7004
7065
|
title: "Create a mission",
|
|
@@ -7364,10 +7425,10 @@ function createPluneToolExecutor(opts) {
|
|
|
7364
7425
|
]);
|
|
7365
7426
|
const projectList = asArray(asRecord(projects.body)["projects"]);
|
|
7366
7427
|
const only = projectList.length === 1 ? asRecord(projectList[0])["id"] : void 0;
|
|
7367
|
-
const
|
|
7428
|
+
const projectId2 = opts.projectId ?? (typeof only === "string" ? only : null);
|
|
7368
7429
|
const total = asRecord(cases.body)["total"];
|
|
7369
7430
|
return {
|
|
7370
|
-
projectId,
|
|
7431
|
+
projectId: projectId2,
|
|
7371
7432
|
projects: projectList,
|
|
7372
7433
|
suites: asArray(asRecord(suites.body)["suites"]),
|
|
7373
7434
|
cases: { total: typeof total === "number" ? total : 0 },
|
|
@@ -7436,6 +7497,18 @@ function createPluneToolExecutor(opts) {
|
|
|
7436
7497
|
const proposed = summary["proposed"];
|
|
7437
7498
|
return { dryRun: false, requested: cases.length, applied: typeof proposed === "number" ? proposed : 0, report: summary };
|
|
7438
7499
|
},
|
|
7500
|
+
async plune_cases_generate(i) {
|
|
7501
|
+
const { dry_run, projectId: projectId2, ...body } = i;
|
|
7502
|
+
const project = projectId2 ?? opts.projectId;
|
|
7503
|
+
if (project === void 0) return fail("invalid_input", NO_PROJECT);
|
|
7504
|
+
return write({ method: "POST", path: path19("/v1/projects/:id/case-generations", project), body: withoutUndefined(body) }, dry_run);
|
|
7505
|
+
},
|
|
7506
|
+
async plune_cases_generation_get(i) {
|
|
7507
|
+
const project = i["projectId"] ?? opts.projectId;
|
|
7508
|
+
if (project === void 0) return fail("invalid_input", NO_PROJECT);
|
|
7509
|
+
const res = await call({ method: "GET", path: path19("/v1/projects/:id/case-generations/:generationId", project, str2(i["generationId"])) });
|
|
7510
|
+
return { generation: asRecord(res.body) };
|
|
7511
|
+
},
|
|
7439
7512
|
async plune_mission_create(i) {
|
|
7440
7513
|
const { dry_run, ...body } = i;
|
|
7441
7514
|
return write({ method: "POST", path: "/v1/missions", body: withoutUndefined(body) }, dry_run);
|
|
@@ -7579,7 +7652,7 @@ function createPluneToolExecutor(opts) {
|
|
|
7579
7652
|
}
|
|
7580
7653
|
};
|
|
7581
7654
|
}
|
|
7582
|
-
var import_node_crypto5, CONFIRM_TTL_MS, READ_ONLY_REFUSAL, readScopeMessage, fail, zodMessage, path19, withoutUndefined, asRecord, asArray, cursorOf, rawWrites;
|
|
7655
|
+
var import_node_crypto5, CONFIRM_TTL_MS, READ_ONLY_REFUSAL, NO_PROJECT, readScopeMessage, fail, zodMessage, path19, withoutUndefined, asRecord, asArray, cursorOf, rawWrites;
|
|
7583
7656
|
var init_execute = __esm({
|
|
7584
7657
|
"src/mcp-tools/execute.ts"() {
|
|
7585
7658
|
"use strict";
|
|
@@ -7590,6 +7663,7 @@ var init_execute = __esm({
|
|
|
7590
7663
|
init_schemas();
|
|
7591
7664
|
CONFIRM_TTL_MS = 5 * 60 * 1e3;
|
|
7592
7665
|
READ_ONLY_REFUSAL = "read-only token";
|
|
7666
|
+
NO_PROJECT = "projectId is needed: the server was started without a project (--project or PLUNE_PROJECT). plune_project_context lists the projects.";
|
|
7593
7667
|
readScopeMessage = (what) => `This token has the read scope, so ${what}. Use a token with the write scope (Plune \u2192 Account \u2192 API tokens) and restart the server.`;
|
|
7594
7668
|
fail = (code, message, extra = {}) => ({
|
|
7595
7669
|
ok: false,
|
|
@@ -7704,13 +7778,13 @@ async function handleMcpServe(flags, deps) {
|
|
|
7704
7778
|
const token = (deps.loadToken ?? loadToken)();
|
|
7705
7779
|
if (token === null) throw new NotLoggedInError();
|
|
7706
7780
|
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
7707
|
-
const
|
|
7781
|
+
const projectId2 = flags.project?.trim() || process.env["PLUNE_PROJECT"]?.trim() || void 0;
|
|
7708
7782
|
const serve = deps.serve ?? (await Promise.resolve().then(() => (init_server(), server_exports))).serveStdio;
|
|
7709
7783
|
await serve({
|
|
7710
7784
|
token,
|
|
7711
7785
|
apiUrl,
|
|
7712
7786
|
version: deps.version,
|
|
7713
|
-
...
|
|
7787
|
+
...projectId2 !== void 0 ? { projectId: projectId2 } : {},
|
|
7714
7788
|
...flags.readOnly === true ? { scope: "read" } : {}
|
|
7715
7789
|
});
|
|
7716
7790
|
}
|
|
@@ -8135,7 +8209,7 @@ function readMarkdown(cwd, file) {
|
|
|
8135
8209
|
if (text2.trim() === "") usage(`${target} is empty.`);
|
|
8136
8210
|
return text2;
|
|
8137
8211
|
}
|
|
8138
|
-
function refusal(out, apiUrl,
|
|
8212
|
+
function refusal(out, apiUrl, projectId2) {
|
|
8139
8213
|
const d = out.detail;
|
|
8140
8214
|
const said = d === "" ? "" : ` (${d})`;
|
|
8141
8215
|
switch (out.status) {
|
|
@@ -8146,7 +8220,7 @@ function refusal(out, apiUrl, projectId) {
|
|
|
8146
8220
|
case 403:
|
|
8147
8221
|
return new RequirementsImportError(`The platform refused: ${d || "read-only member"}. Importing needs an owner or a member of the project.`, 1);
|
|
8148
8222
|
case 404:
|
|
8149
|
-
return /^project '.*' not found$/.test(d) ? new RequirementsImportError(`Project ${
|
|
8223
|
+
return /^project '.*' not found$/.test(d) ? new RequirementsImportError(`Project ${projectId2} was not found for this token. A token only works in the project it was minted in; pass --project <id> or use a token from that project.`, 1) : new RequirementsImportError(SUPPORT, 1);
|
|
8150
8224
|
case 409:
|
|
8151
8225
|
return new RequirementsImportError(`A requirement key was taken by another change while the import ran. Nothing was written; run it again${said}.`, 1);
|
|
8152
8226
|
case 413:
|
|
@@ -8176,15 +8250,15 @@ async function handleRequirementsImport(deps) {
|
|
|
8176
8250
|
const token = (deps.loadToken ?? loadToken)();
|
|
8177
8251
|
if (token === null) throw new RequirementsImportError('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.', 2);
|
|
8178
8252
|
const client = createClient({ apiUrl, token, ...deps.fetchImpl !== void 0 ? { fetchImpl: deps.fetchImpl } : {} });
|
|
8179
|
-
const
|
|
8180
|
-
const base = `/v1/projects/${encodeURIComponent(
|
|
8253
|
+
const projectId2 = await resolveProject(client, deps.project, apiUrl);
|
|
8254
|
+
const base = `/v1/projects/${encodeURIComponent(projectId2)}/requirements/import`;
|
|
8181
8255
|
if (mapping === null) {
|
|
8182
8256
|
const out2 = await client.post(`${base}/discover`, { markdown });
|
|
8183
|
-
if (!out2.ok) throw refusal(out2, apiUrl,
|
|
8257
|
+
if (!out2.ok) throw refusal(out2, apiUrl, projectId2);
|
|
8184
8258
|
return { step: "discover", result: out2.body };
|
|
8185
8259
|
}
|
|
8186
8260
|
const out = await client.post(base, { markdown, dryRun: deps.yes !== true, ...mapping });
|
|
8187
|
-
if (!out.ok) throw refusal(out, apiUrl,
|
|
8261
|
+
if (!out.ok) throw refusal(out, apiUrl, projectId2);
|
|
8188
8262
|
return { step: "import", report: out.body };
|
|
8189
8263
|
}
|
|
8190
8264
|
function formatDiscover(file, r) {
|
|
@@ -8340,15 +8414,15 @@ async function handleRequirementsLinks(deps) {
|
|
|
8340
8414
|
const token = (deps.loadToken ?? loadToken)();
|
|
8341
8415
|
if (token === null) throw new RequirementsImportError('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.', 2);
|
|
8342
8416
|
const client = createClient({ apiUrl, token, ...deps.fetchImpl !== void 0 ? { fetchImpl: deps.fetchImpl } : {} });
|
|
8343
|
-
const
|
|
8344
|
-
const base = `/v1/projects/${encodeURIComponent(
|
|
8417
|
+
const projectId2 = await resolveProject(client, deps.project, apiUrl);
|
|
8418
|
+
const base = `/v1/projects/${encodeURIComponent(projectId2)}/requirements/import`;
|
|
8345
8419
|
if (mapping === null) {
|
|
8346
8420
|
const out2 = await client.post(`${base}/discover`, { markdown });
|
|
8347
|
-
if (!out2.ok) throw refusal(out2, apiUrl,
|
|
8421
|
+
if (!out2.ok) throw refusal(out2, apiUrl, projectId2);
|
|
8348
8422
|
return { step: "discover", result: out2.body };
|
|
8349
8423
|
}
|
|
8350
8424
|
const out = await client.post(`${base}/links`, { markdown, dryRun: deps.yes !== true, ...mapping });
|
|
8351
|
-
if (!out.ok) throw refusal(out, apiUrl,
|
|
8425
|
+
if (!out.ok) throw refusal(out, apiUrl, projectId2);
|
|
8352
8426
|
return { step: "links", report: out.body };
|
|
8353
8427
|
}
|
|
8354
8428
|
function formatLinksDiscover(file, r) {
|
package/dist/index.cjs
CHANGED
|
@@ -1686,7 +1686,31 @@ function loadDataset(ref, baseDir) {
|
|
|
1686
1686
|
return ref.examples;
|
|
1687
1687
|
}
|
|
1688
1688
|
const content = fs4.readFileSync(path3.resolve(baseDir, ref), "utf8");
|
|
1689
|
-
|
|
1689
|
+
const rows = [];
|
|
1690
|
+
content.split(/\r?\n/).forEach((line, i) => {
|
|
1691
|
+
if (line.trim().length === 0) return;
|
|
1692
|
+
let row;
|
|
1693
|
+
try {
|
|
1694
|
+
row = JSON.parse(line.trim());
|
|
1695
|
+
} catch (err) {
|
|
1696
|
+
throw new RunConfigError(`${ref}:${i + 1}: not valid JSON (${err.message})`);
|
|
1697
|
+
}
|
|
1698
|
+
const vars = isObject(row) ? row["vars"] : void 0;
|
|
1699
|
+
if (!isObject(vars)) {
|
|
1700
|
+
const why = isObject(row) && "vars" in row ? `vars: Invalid input: expected record, received ${typeName(vars)}` : 'this row has no "vars"';
|
|
1701
|
+
throw new RunConfigError(
|
|
1702
|
+
`${ref}:${i + 1}: a dataset row must be { "vars": { ... } }; ${why}`
|
|
1703
|
+
);
|
|
1704
|
+
}
|
|
1705
|
+
rows.push(row);
|
|
1706
|
+
});
|
|
1707
|
+
return rows;
|
|
1708
|
+
}
|
|
1709
|
+
function isObject(value) {
|
|
1710
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1711
|
+
}
|
|
1712
|
+
function typeName(value) {
|
|
1713
|
+
return value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
|
|
1690
1714
|
}
|
|
1691
1715
|
|
|
1692
1716
|
// src/cli/commands/run.ts
|
package/dist/index.js
CHANGED
|
@@ -1626,7 +1626,31 @@ function loadDataset(ref, baseDir) {
|
|
|
1626
1626
|
return ref.examples;
|
|
1627
1627
|
}
|
|
1628
1628
|
const content = fs4.readFileSync(path3.resolve(baseDir, ref), "utf8");
|
|
1629
|
-
|
|
1629
|
+
const rows = [];
|
|
1630
|
+
content.split(/\r?\n/).forEach((line, i) => {
|
|
1631
|
+
if (line.trim().length === 0) return;
|
|
1632
|
+
let row;
|
|
1633
|
+
try {
|
|
1634
|
+
row = JSON.parse(line.trim());
|
|
1635
|
+
} catch (err) {
|
|
1636
|
+
throw new RunConfigError(`${ref}:${i + 1}: not valid JSON (${err.message})`);
|
|
1637
|
+
}
|
|
1638
|
+
const vars = isObject(row) ? row["vars"] : void 0;
|
|
1639
|
+
if (!isObject(vars)) {
|
|
1640
|
+
const why = isObject(row) && "vars" in row ? `vars: Invalid input: expected record, received ${typeName(vars)}` : 'this row has no "vars"';
|
|
1641
|
+
throw new RunConfigError(
|
|
1642
|
+
`${ref}:${i + 1}: a dataset row must be { "vars": { ... } }; ${why}`
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
rows.push(row);
|
|
1646
|
+
});
|
|
1647
|
+
return rows;
|
|
1648
|
+
}
|
|
1649
|
+
function isObject(value) {
|
|
1650
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1651
|
+
}
|
|
1652
|
+
function typeName(value) {
|
|
1653
|
+
return value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
|
|
1630
1654
|
}
|
|
1631
1655
|
|
|
1632
1656
|
// src/cli/commands/run.ts
|
package/dist/mcp-tools.cjs
CHANGED
|
@@ -163,6 +163,21 @@ var proposedCase = import_zod.z.strictObject({
|
|
|
163
163
|
source: import_zod.z.strictObject({ kind: import_zod.z.enum(SOURCE_REF_KINDS), value: import_zod.z.string().min(1).max(100) }).optional().describe("What you read to design it"),
|
|
164
164
|
rationale: import_zod.z.string().min(1).max(400).optional().describe("Why the case is worth having")
|
|
165
165
|
});
|
|
166
|
+
var CASE_GENERATION_TITLE_MAX = 300;
|
|
167
|
+
var CASE_GENERATION_MAX_CASES = 12;
|
|
168
|
+
var CASE_GENERATION_DEFAULT_CASES = 8;
|
|
169
|
+
var CASE_GENERATION_MIN_WORDS = 20;
|
|
170
|
+
var CASE_GENERATION_TEXT_MAX = 12e3;
|
|
171
|
+
var caseGenerationBody = {
|
|
172
|
+
input: import_zod.z.strictObject({
|
|
173
|
+
kind: import_zod.z.literal("pasted-text"),
|
|
174
|
+
text: import_zod.z.string().describe(`The requirement, story or ticket text, at least ${CASE_GENERATION_MIN_WORDS} words and at most ${CASE_GENERATION_TEXT_MAX} characters`),
|
|
175
|
+
title: import_zod.z.string().trim().max(CASE_GENERATION_TITLE_MAX).optional()
|
|
176
|
+
}),
|
|
177
|
+
maxCases: import_zod.z.number().int().min(1).max(CASE_GENERATION_MAX_CASES).optional().describe(`How many proposals at most; ${CASE_GENERATION_DEFAULT_CASES} when left out`),
|
|
178
|
+
language: import_zod.z.string().trim().min(2).max(35).optional().describe("The cases' language; the project's AI language when left out"),
|
|
179
|
+
confirmDuplicates: import_zod.z.boolean().optional().describe("true: go ahead although cases or proposals from this text exist (the platform answers 409 duplicate_source first)")
|
|
180
|
+
};
|
|
166
181
|
var API_METHODS = ["GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"];
|
|
167
182
|
var proposedEndpoint = import_zod.z.strictObject({
|
|
168
183
|
method: import_zod.z.enum(API_METHODS),
|
|
@@ -219,6 +234,7 @@ var listPage = {
|
|
|
219
234
|
cursor: import_zod2.z.string().min(1).optional().describe("nextCursor of the previous page")
|
|
220
235
|
};
|
|
221
236
|
var missionId = id("The mission id");
|
|
237
|
+
var projectId = import_zod2.z.string().min(1).optional().describe("The project; the one the server was started with when left out");
|
|
222
238
|
var PLUNE_TOOLS = [
|
|
223
239
|
define({
|
|
224
240
|
name: "plune_project_context",
|
|
@@ -282,7 +298,7 @@ var PLUNE_TOOLS = [
|
|
|
282
298
|
define({
|
|
283
299
|
name: "plune_review_decide",
|
|
284
300
|
title: "Decide queue entries",
|
|
285
|
-
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.",
|
|
301
|
+
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. An entry skipped as policy_refused was refused by the review policy of the project (for example, the person who proposed it cannot approve it): do not retry it \u2014 it needs another reviewer; its detail says why.",
|
|
286
302
|
input: import_zod2.z.strictObject({
|
|
287
303
|
itemIds: import_zod2.z.array(import_zod2.z.string().min(1)).min(1).max(BATCH_MAX).refine((a) => new Set(a).size === a.length, { message: "an id is named once" }),
|
|
288
304
|
verdict: import_zod2.z.enum(["approved", "rejected"]),
|
|
@@ -314,6 +330,26 @@ var PLUNE_TOOLS = [
|
|
|
314
330
|
visibility: "core",
|
|
315
331
|
endpoints: ["POST /v1/ingest/cairn"]
|
|
316
332
|
}),
|
|
333
|
+
define({
|
|
334
|
+
name: "plune_cases_generate",
|
|
335
|
+
title: "Generate cases from pasted text",
|
|
336
|
+
description: "Generate test case proposals from pasted text \u2014 a requirement, a user story, the text of a ticket \u2014 with the project's AI. It spends the project's AI budget. The proposals land in the review queue and nothing becomes a case until a person approves it. The run is queued: keep its id and poll it with plune_cases_generation_get until it is done. A Jira ticket by its key is not taken here: paste its text.",
|
|
337
|
+
input: import_zod2.z.strictObject({ projectId, ...caseGenerationBody, dry_run: dryRun }),
|
|
338
|
+
output: writeOutput,
|
|
339
|
+
write: true,
|
|
340
|
+
visibility: "core",
|
|
341
|
+
endpoints: ["POST /v1/projects/:id/case-generations"]
|
|
342
|
+
}),
|
|
343
|
+
define({
|
|
344
|
+
name: "plune_cases_generation_get",
|
|
345
|
+
title: "Read a case generation",
|
|
346
|
+
description: "Check where a run of cases from pasted text stands \u2014 queued, running, done, failed or cancelled \u2014 with how many proposals it made, how many it skipped, and what it cost.",
|
|
347
|
+
input: import_zod2.z.strictObject({ projectId, generationId: id("The id the start answered with") }),
|
|
348
|
+
output: import_zod2.z.object({ generation: entity }),
|
|
349
|
+
write: false,
|
|
350
|
+
visibility: "core",
|
|
351
|
+
endpoints: ["GET /v1/projects/:id/case-generations/:generationId"]
|
|
352
|
+
}),
|
|
317
353
|
define({
|
|
318
354
|
name: "plune_mission_create",
|
|
319
355
|
title: "Create a mission",
|
|
@@ -607,6 +643,7 @@ function createPluneApi(opts) {
|
|
|
607
643
|
// src/mcp-tools/execute.ts
|
|
608
644
|
var CONFIRM_TTL_MS = 5 * 60 * 1e3;
|
|
609
645
|
var READ_ONLY_REFUSAL = "read-only token";
|
|
646
|
+
var NO_PROJECT = "projectId is needed: the server was started without a project (--project or PLUNE_PROJECT). plune_project_context lists the projects.";
|
|
610
647
|
var 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.`;
|
|
611
648
|
var fail = (code, message, extra = {}) => ({
|
|
612
649
|
ok: false,
|
|
@@ -701,10 +738,10 @@ function createPluneToolExecutor(opts) {
|
|
|
701
738
|
]);
|
|
702
739
|
const projectList = asArray(asRecord(projects.body)["projects"]);
|
|
703
740
|
const only = projectList.length === 1 ? asRecord(projectList[0])["id"] : void 0;
|
|
704
|
-
const
|
|
741
|
+
const projectId2 = opts.projectId ?? (typeof only === "string" ? only : null);
|
|
705
742
|
const total = asRecord(cases.body)["total"];
|
|
706
743
|
return {
|
|
707
|
-
projectId,
|
|
744
|
+
projectId: projectId2,
|
|
708
745
|
projects: projectList,
|
|
709
746
|
suites: asArray(asRecord(suites.body)["suites"]),
|
|
710
747
|
cases: { total: typeof total === "number" ? total : 0 },
|
|
@@ -773,6 +810,18 @@ function createPluneToolExecutor(opts) {
|
|
|
773
810
|
const proposed = summary["proposed"];
|
|
774
811
|
return { dryRun: false, requested: cases.length, applied: typeof proposed === "number" ? proposed : 0, report: summary };
|
|
775
812
|
},
|
|
813
|
+
async plune_cases_generate(i) {
|
|
814
|
+
const { dry_run, projectId: projectId2, ...body } = i;
|
|
815
|
+
const project = projectId2 ?? opts.projectId;
|
|
816
|
+
if (project === void 0) return fail("invalid_input", NO_PROJECT);
|
|
817
|
+
return write({ method: "POST", path: path("/v1/projects/:id/case-generations", project), body: withoutUndefined(body) }, dry_run);
|
|
818
|
+
},
|
|
819
|
+
async plune_cases_generation_get(i) {
|
|
820
|
+
const project = i["projectId"] ?? opts.projectId;
|
|
821
|
+
if (project === void 0) return fail("invalid_input", NO_PROJECT);
|
|
822
|
+
const res = await call({ method: "GET", path: path("/v1/projects/:id/case-generations/:generationId", project, str(i["generationId"])) });
|
|
823
|
+
return { generation: asRecord(res.body) };
|
|
824
|
+
},
|
|
776
825
|
async plune_mission_create(i) {
|
|
777
826
|
const { dry_run, ...body } = i;
|
|
778
827
|
return write({ method: "POST", path: "/v1/missions", body: withoutUndefined(body) }, dry_run);
|
package/dist/mcp-tools.d.cts
CHANGED
|
@@ -208,6 +208,53 @@ declare const PLUNE_TOOLS: readonly [{
|
|
|
208
208
|
write: true;
|
|
209
209
|
visibility: "core";
|
|
210
210
|
endpoints: string[];
|
|
211
|
+
}, {
|
|
212
|
+
name: string;
|
|
213
|
+
title: string;
|
|
214
|
+
description: string;
|
|
215
|
+
input: z.ZodObject<{
|
|
216
|
+
dry_run: z.ZodOptional<z.ZodBoolean>;
|
|
217
|
+
input: z.ZodObject<{
|
|
218
|
+
kind: z.ZodLiteral<"pasted-text">;
|
|
219
|
+
text: z.ZodString;
|
|
220
|
+
title: z.ZodOptional<z.ZodString>;
|
|
221
|
+
}, z.core.$strict>;
|
|
222
|
+
maxCases: z.ZodOptional<z.ZodNumber>;
|
|
223
|
+
language: z.ZodOptional<z.ZodString>;
|
|
224
|
+
confirmDuplicates: z.ZodOptional<z.ZodBoolean>;
|
|
225
|
+
projectId: z.ZodOptional<z.ZodString>;
|
|
226
|
+
}, z.core.$strict>;
|
|
227
|
+
output: z.ZodObject<{
|
|
228
|
+
dryRun: z.ZodBoolean;
|
|
229
|
+
request: z.ZodOptional<z.ZodObject<{
|
|
230
|
+
method: z.ZodString;
|
|
231
|
+
path: z.ZodString;
|
|
232
|
+
query: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
233
|
+
body: z.ZodOptional<z.ZodUnknown>;
|
|
234
|
+
}, z.core.$strip>>;
|
|
235
|
+
result: z.ZodOptional<z.ZodObject<{
|
|
236
|
+
id: z.ZodOptional<z.ZodString>;
|
|
237
|
+
}, z.core.$loose>>;
|
|
238
|
+
}, z.core.$strip>;
|
|
239
|
+
write: true;
|
|
240
|
+
visibility: "core";
|
|
241
|
+
endpoints: string[];
|
|
242
|
+
}, {
|
|
243
|
+
name: string;
|
|
244
|
+
title: string;
|
|
245
|
+
description: string;
|
|
246
|
+
input: z.ZodObject<{
|
|
247
|
+
projectId: z.ZodOptional<z.ZodString>;
|
|
248
|
+
generationId: z.ZodString;
|
|
249
|
+
}, z.core.$strict>;
|
|
250
|
+
output: z.ZodObject<{
|
|
251
|
+
generation: z.ZodObject<{
|
|
252
|
+
id: z.ZodOptional<z.ZodString>;
|
|
253
|
+
}, z.core.$loose>;
|
|
254
|
+
}, z.core.$strip>;
|
|
255
|
+
write: false;
|
|
256
|
+
visibility: "core";
|
|
257
|
+
endpoints: string[];
|
|
211
258
|
}, {
|
|
212
259
|
name: string;
|
|
213
260
|
title: string;
|
package/dist/mcp-tools.d.ts
CHANGED
|
@@ -208,6 +208,53 @@ declare const PLUNE_TOOLS: readonly [{
|
|
|
208
208
|
write: true;
|
|
209
209
|
visibility: "core";
|
|
210
210
|
endpoints: string[];
|
|
211
|
+
}, {
|
|
212
|
+
name: string;
|
|
213
|
+
title: string;
|
|
214
|
+
description: string;
|
|
215
|
+
input: z.ZodObject<{
|
|
216
|
+
dry_run: z.ZodOptional<z.ZodBoolean>;
|
|
217
|
+
input: z.ZodObject<{
|
|
218
|
+
kind: z.ZodLiteral<"pasted-text">;
|
|
219
|
+
text: z.ZodString;
|
|
220
|
+
title: z.ZodOptional<z.ZodString>;
|
|
221
|
+
}, z.core.$strict>;
|
|
222
|
+
maxCases: z.ZodOptional<z.ZodNumber>;
|
|
223
|
+
language: z.ZodOptional<z.ZodString>;
|
|
224
|
+
confirmDuplicates: z.ZodOptional<z.ZodBoolean>;
|
|
225
|
+
projectId: z.ZodOptional<z.ZodString>;
|
|
226
|
+
}, z.core.$strict>;
|
|
227
|
+
output: z.ZodObject<{
|
|
228
|
+
dryRun: z.ZodBoolean;
|
|
229
|
+
request: z.ZodOptional<z.ZodObject<{
|
|
230
|
+
method: z.ZodString;
|
|
231
|
+
path: z.ZodString;
|
|
232
|
+
query: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
233
|
+
body: z.ZodOptional<z.ZodUnknown>;
|
|
234
|
+
}, z.core.$strip>>;
|
|
235
|
+
result: z.ZodOptional<z.ZodObject<{
|
|
236
|
+
id: z.ZodOptional<z.ZodString>;
|
|
237
|
+
}, z.core.$loose>>;
|
|
238
|
+
}, z.core.$strip>;
|
|
239
|
+
write: true;
|
|
240
|
+
visibility: "core";
|
|
241
|
+
endpoints: string[];
|
|
242
|
+
}, {
|
|
243
|
+
name: string;
|
|
244
|
+
title: string;
|
|
245
|
+
description: string;
|
|
246
|
+
input: z.ZodObject<{
|
|
247
|
+
projectId: z.ZodOptional<z.ZodString>;
|
|
248
|
+
generationId: z.ZodString;
|
|
249
|
+
}, z.core.$strict>;
|
|
250
|
+
output: z.ZodObject<{
|
|
251
|
+
generation: z.ZodObject<{
|
|
252
|
+
id: z.ZodOptional<z.ZodString>;
|
|
253
|
+
}, z.core.$loose>;
|
|
254
|
+
}, z.core.$strip>;
|
|
255
|
+
write: false;
|
|
256
|
+
visibility: "core";
|
|
257
|
+
endpoints: string[];
|
|
211
258
|
}, {
|
|
212
259
|
name: string;
|
|
213
260
|
title: string;
|
package/dist/mcp-tools.js
CHANGED
|
@@ -127,6 +127,21 @@ var proposedCase = z.strictObject({
|
|
|
127
127
|
source: z.strictObject({ kind: z.enum(SOURCE_REF_KINDS), value: z.string().min(1).max(100) }).optional().describe("What you read to design it"),
|
|
128
128
|
rationale: z.string().min(1).max(400).optional().describe("Why the case is worth having")
|
|
129
129
|
});
|
|
130
|
+
var CASE_GENERATION_TITLE_MAX = 300;
|
|
131
|
+
var CASE_GENERATION_MAX_CASES = 12;
|
|
132
|
+
var CASE_GENERATION_DEFAULT_CASES = 8;
|
|
133
|
+
var CASE_GENERATION_MIN_WORDS = 20;
|
|
134
|
+
var CASE_GENERATION_TEXT_MAX = 12e3;
|
|
135
|
+
var caseGenerationBody = {
|
|
136
|
+
input: z.strictObject({
|
|
137
|
+
kind: z.literal("pasted-text"),
|
|
138
|
+
text: z.string().describe(`The requirement, story or ticket text, at least ${CASE_GENERATION_MIN_WORDS} words and at most ${CASE_GENERATION_TEXT_MAX} characters`),
|
|
139
|
+
title: z.string().trim().max(CASE_GENERATION_TITLE_MAX).optional()
|
|
140
|
+
}),
|
|
141
|
+
maxCases: z.number().int().min(1).max(CASE_GENERATION_MAX_CASES).optional().describe(`How many proposals at most; ${CASE_GENERATION_DEFAULT_CASES} when left out`),
|
|
142
|
+
language: z.string().trim().min(2).max(35).optional().describe("The cases' language; the project's AI language when left out"),
|
|
143
|
+
confirmDuplicates: z.boolean().optional().describe("true: go ahead although cases or proposals from this text exist (the platform answers 409 duplicate_source first)")
|
|
144
|
+
};
|
|
130
145
|
var API_METHODS = ["GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"];
|
|
131
146
|
var proposedEndpoint = z.strictObject({
|
|
132
147
|
method: z.enum(API_METHODS),
|
|
@@ -183,6 +198,7 @@ var listPage = {
|
|
|
183
198
|
cursor: z2.string().min(1).optional().describe("nextCursor of the previous page")
|
|
184
199
|
};
|
|
185
200
|
var missionId = id("The mission id");
|
|
201
|
+
var projectId = z2.string().min(1).optional().describe("The project; the one the server was started with when left out");
|
|
186
202
|
var PLUNE_TOOLS = [
|
|
187
203
|
define({
|
|
188
204
|
name: "plune_project_context",
|
|
@@ -246,7 +262,7 @@ var PLUNE_TOOLS = [
|
|
|
246
262
|
define({
|
|
247
263
|
name: "plune_review_decide",
|
|
248
264
|
title: "Decide queue entries",
|
|
249
|
-
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.",
|
|
265
|
+
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. An entry skipped as policy_refused was refused by the review policy of the project (for example, the person who proposed it cannot approve it): do not retry it \u2014 it needs another reviewer; its detail says why.",
|
|
250
266
|
input: z2.strictObject({
|
|
251
267
|
itemIds: z2.array(z2.string().min(1)).min(1).max(BATCH_MAX).refine((a) => new Set(a).size === a.length, { message: "an id is named once" }),
|
|
252
268
|
verdict: z2.enum(["approved", "rejected"]),
|
|
@@ -278,6 +294,26 @@ var PLUNE_TOOLS = [
|
|
|
278
294
|
visibility: "core",
|
|
279
295
|
endpoints: ["POST /v1/ingest/cairn"]
|
|
280
296
|
}),
|
|
297
|
+
define({
|
|
298
|
+
name: "plune_cases_generate",
|
|
299
|
+
title: "Generate cases from pasted text",
|
|
300
|
+
description: "Generate test case proposals from pasted text \u2014 a requirement, a user story, the text of a ticket \u2014 with the project's AI. It spends the project's AI budget. The proposals land in the review queue and nothing becomes a case until a person approves it. The run is queued: keep its id and poll it with plune_cases_generation_get until it is done. A Jira ticket by its key is not taken here: paste its text.",
|
|
301
|
+
input: z2.strictObject({ projectId, ...caseGenerationBody, dry_run: dryRun }),
|
|
302
|
+
output: writeOutput,
|
|
303
|
+
write: true,
|
|
304
|
+
visibility: "core",
|
|
305
|
+
endpoints: ["POST /v1/projects/:id/case-generations"]
|
|
306
|
+
}),
|
|
307
|
+
define({
|
|
308
|
+
name: "plune_cases_generation_get",
|
|
309
|
+
title: "Read a case generation",
|
|
310
|
+
description: "Check where a run of cases from pasted text stands \u2014 queued, running, done, failed or cancelled \u2014 with how many proposals it made, how many it skipped, and what it cost.",
|
|
311
|
+
input: z2.strictObject({ projectId, generationId: id("The id the start answered with") }),
|
|
312
|
+
output: z2.object({ generation: entity }),
|
|
313
|
+
write: false,
|
|
314
|
+
visibility: "core",
|
|
315
|
+
endpoints: ["GET /v1/projects/:id/case-generations/:generationId"]
|
|
316
|
+
}),
|
|
281
317
|
define({
|
|
282
318
|
name: "plune_mission_create",
|
|
283
319
|
title: "Create a mission",
|
|
@@ -571,6 +607,7 @@ function createPluneApi(opts) {
|
|
|
571
607
|
// src/mcp-tools/execute.ts
|
|
572
608
|
var CONFIRM_TTL_MS = 5 * 60 * 1e3;
|
|
573
609
|
var READ_ONLY_REFUSAL = "read-only token";
|
|
610
|
+
var NO_PROJECT = "projectId is needed: the server was started without a project (--project or PLUNE_PROJECT). plune_project_context lists the projects.";
|
|
574
611
|
var 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.`;
|
|
575
612
|
var fail = (code, message, extra = {}) => ({
|
|
576
613
|
ok: false,
|
|
@@ -665,10 +702,10 @@ function createPluneToolExecutor(opts) {
|
|
|
665
702
|
]);
|
|
666
703
|
const projectList = asArray(asRecord(projects.body)["projects"]);
|
|
667
704
|
const only = projectList.length === 1 ? asRecord(projectList[0])["id"] : void 0;
|
|
668
|
-
const
|
|
705
|
+
const projectId2 = opts.projectId ?? (typeof only === "string" ? only : null);
|
|
669
706
|
const total = asRecord(cases.body)["total"];
|
|
670
707
|
return {
|
|
671
|
-
projectId,
|
|
708
|
+
projectId: projectId2,
|
|
672
709
|
projects: projectList,
|
|
673
710
|
suites: asArray(asRecord(suites.body)["suites"]),
|
|
674
711
|
cases: { total: typeof total === "number" ? total : 0 },
|
|
@@ -737,6 +774,18 @@ function createPluneToolExecutor(opts) {
|
|
|
737
774
|
const proposed = summary["proposed"];
|
|
738
775
|
return { dryRun: false, requested: cases.length, applied: typeof proposed === "number" ? proposed : 0, report: summary };
|
|
739
776
|
},
|
|
777
|
+
async plune_cases_generate(i) {
|
|
778
|
+
const { dry_run, projectId: projectId2, ...body } = i;
|
|
779
|
+
const project = projectId2 ?? opts.projectId;
|
|
780
|
+
if (project === void 0) return fail("invalid_input", NO_PROJECT);
|
|
781
|
+
return write({ method: "POST", path: path("/v1/projects/:id/case-generations", project), body: withoutUndefined(body) }, dry_run);
|
|
782
|
+
},
|
|
783
|
+
async plune_cases_generation_get(i) {
|
|
784
|
+
const project = i["projectId"] ?? opts.projectId;
|
|
785
|
+
if (project === void 0) return fail("invalid_input", NO_PROJECT);
|
|
786
|
+
const res = await call({ method: "GET", path: path("/v1/projects/:id/case-generations/:generationId", project, str(i["generationId"])) });
|
|
787
|
+
return { generation: asRecord(res.body) };
|
|
788
|
+
},
|
|
740
789
|
async plune_mission_create(i) {
|
|
741
790
|
const { dry_run, ...body } = i;
|
|
742
791
|
return write({ method: "POST", path: "/v1/missions", body: withoutUndefined(body) }, dry_run);
|
package/package.json
CHANGED