@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 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"?: "..." }`. The
74
- provider API key is read from the environment based on `provider.type`:
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
- return content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => JSON.parse(line));
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 isObject(value2) {
5690
+ function isObject2(value2) {
5666
5691
  return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
5667
5692
  }
5668
5693
  function isK6Summary(parsed) {
5669
- if (!isObject(parsed) || !isObject(parsed["metrics"])) return false;
5670
- if (isObject(parsed["root_group"])) return true;
5671
- return Object.values(parsed["metrics"]).some((m) => isObject(m) && (typeof m["type"] === "string" || isObject(m["thresholds"])));
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 (isObject(verdict) && typeof verdict["ok"] === "boolean") return verdict["ok"];
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 = isObject(metric.values) ? metric.values : metric;
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 (!isObject(parsed) || !isObject(parsed["metrics"])) {
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 (!isObject(raw) || !isObject(raw["thresholds"])) continue;
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 isObject2(value2) {
5762
+ function isObject3(value2) {
5738
5763
  return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
5739
5764
  }
5740
5765
  function isLighthouseReport(parsed) {
5741
- return isObject2(parsed) && typeof parsed["lighthouseVersion"] === "string" && isObject2(parsed["categories"]);
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 = isObject2(timing) && typeof timing["total"] === "number" && timing["total"] >= 0 ? Math.round(timing["total"]) : 0;
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 (!isObject2(err)) return void 0;
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 = isObject2(settings) && typeof settings["formFactor"] === "string" && settings["formFactor"] !== "" ? settings["formFactor"] : void 0;
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]) => isObject2(raw)).map(([key, raw]) => idOf(key, 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 (!isObject2(raw)) continue;
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 projectId = opts.projectId ?? (typeof only === "string" ? only : null);
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 projectId = flags.project?.trim() || process.env["PLUNE_PROJECT"]?.trim() || void 0;
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
- ...projectId !== void 0 ? { projectId } : {},
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, projectId) {
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 ${projectId} 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);
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 projectId = await resolveProject(client, deps.project, apiUrl);
8180
- const base = `/v1/projects/${encodeURIComponent(projectId)}/requirements/import`;
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, projectId);
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, projectId);
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 projectId = await resolveProject(client, deps.project, apiUrl);
8344
- const base = `/v1/projects/${encodeURIComponent(projectId)}/requirements/import`;
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, projectId);
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, projectId);
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
- return content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => JSON.parse(line));
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
- return content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => JSON.parse(line));
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
@@ -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 projectId = opts.projectId ?? (typeof only === "string" ? only : null);
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);
@@ -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;
@@ -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 projectId = opts.projectId ?? (typeof only === "string" ? only : null);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plune-ai/cli",
3
- "version": "0.34.1",
3
+ "version": "0.34.3",
4
4
  "description": "AI-powered assertion testing for LLM apps — write assertions, run them against any provider, get a pass/fail report (local, CI, or diff).",
5
5
  "keywords": [
6
6
  "ai",