@plune-ai/cli 0.34.1 → 0.34.2

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
@@ -631,6 +631,16 @@ once; the rest it finds with `plune_discover_tools`. Every write tool takes `dry
631
631
  100 items, and the agent is held to the platform's agent rules: a proposed case lands in the review
632
632
  queue, a bulk decision needs a confirm, and nothing is deleted through MCP.
633
633
 
634
+ `plune_cases_generate` asks the project's AI for case proposals from pasted text (a requirement, a
635
+ story, a ticket's text; 20 words to 12 000 characters, up to 12 proposals, 8 by default). It spends the
636
+ project's AI budget, and its proposals wait in the review queue like any other until a person approves
637
+ them. The run is queued: the tool answers with its id, and `plune_cases_generation_get` reads where it
638
+ stands, what it produced and what it cost. A Jira ticket by its key is not taken over MCP: paste its
639
+ text. The platform's refusals (409 `duplicate_source` until `confirmDuplicates: true`, 422 for text
640
+ too short or too long, 429 limits, 503 / 409 when the project's AI is not set up) come back with
641
+ their `code` as the platform sent them. Both tools work in the server's `--project` unless the call
642
+ names a `projectId`.
643
+
634
644
  Claude Code:
635
645
 
636
646
  ```bash
package/dist/cli.cjs CHANGED
@@ -6698,7 +6698,7 @@ var init_logout = __esm({
6698
6698
  });
6699
6699
 
6700
6700
  // src/mcp-tools/schemas.ts
6701
- var import_zod4, BATCH_MAX2, isHttp, httpLink, id, dryRun, MISSION_STATES, MISSION_DOCUMENTATION, MISSION_LINK_KINDS, missionName, missionSummary, missionUrl, missionFields, sourceTitle, sourceVersion, linkSource, fileSource, ACCESS_KINDS, questionText, questionInput, STRATEGY_SOURCE_KINDS, TEST_LEVELS, QUALITY_CHARACTERISTICS, TEST_TECHNIQUES, RISK_SCALE, itemFields, strategyItem, items, unique, linkIds, share, strategyContent, SOURCE_REF_KINDS, proposedCase, API_METHODS, proposedEndpoint, REVIEW_STATES, REVIEW_KINDS, REJECTION_CATEGORIES, entity, plannedRequest, writeOutput, batchOutput, bulkPreview;
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, 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
6702
  var init_schemas = __esm({
6703
6703
  "src/mcp-tools/schemas.ts"() {
6704
6704
  "use strict";
@@ -6828,6 +6828,21 @@ var init_schemas = __esm({
6828
6828
  source: import_zod4.z.strictObject({ kind: import_zod4.z.enum(SOURCE_REF_KINDS), value: import_zod4.z.string().min(1).max(100) }).optional().describe("What you read to design it"),
6829
6829
  rationale: import_zod4.z.string().min(1).max(400).optional().describe("Why the case is worth having")
6830
6830
  });
6831
+ CASE_GENERATION_TITLE_MAX = 300;
6832
+ CASE_GENERATION_MAX_CASES = 12;
6833
+ CASE_GENERATION_DEFAULT_CASES = 8;
6834
+ CASE_GENERATION_MIN_WORDS = 20;
6835
+ CASE_GENERATION_TEXT_MAX = 12e3;
6836
+ caseGenerationBody = {
6837
+ input: import_zod4.z.strictObject({
6838
+ kind: import_zod4.z.literal("pasted-text"),
6839
+ 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`),
6840
+ title: import_zod4.z.string().trim().max(CASE_GENERATION_TITLE_MAX).optional()
6841
+ }),
6842
+ 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`),
6843
+ language: import_zod4.z.string().trim().min(2).max(35).optional().describe("The cases' language; the project's AI language when left out"),
6844
+ 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)")
6845
+ };
6831
6846
  API_METHODS = ["GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"];
6832
6847
  proposedEndpoint = import_zod4.z.strictObject({
6833
6848
  method: import_zod4.z.enum(API_METHODS),
@@ -6890,7 +6905,7 @@ function isPluneToolName(name) {
6890
6905
  function defaultVisibleTools(scope) {
6891
6906
  return PLUNE_TOOLS.filter((t) => t.visibility === "core" && !(scope === "read" && t.write)).map((t) => t.name);
6892
6907
  }
6893
- var import_zod5, MAX_VISIBLE_TOOLS, define, listPage, missionId, PLUNE_TOOLS, PLUNE_TOOL_NAMES, BY_NAME;
6908
+ var import_zod5, MAX_VISIBLE_TOOLS, define, listPage, missionId, projectId, PLUNE_TOOLS, PLUNE_TOOL_NAMES, BY_NAME;
6894
6909
  var init_contract = __esm({
6895
6910
  "src/mcp-tools/contract.ts"() {
6896
6911
  "use strict";
@@ -6904,6 +6919,7 @@ var init_contract = __esm({
6904
6919
  cursor: import_zod5.z.string().min(1).optional().describe("nextCursor of the previous page")
6905
6920
  };
6906
6921
  missionId = id("The mission id");
6922
+ projectId = import_zod5.z.string().min(1).optional().describe("The project; the one the server was started with when left out");
6907
6923
  PLUNE_TOOLS = [
6908
6924
  define({
6909
6925
  name: "plune_project_context",
@@ -6999,6 +7015,26 @@ var init_contract = __esm({
6999
7015
  visibility: "core",
7000
7016
  endpoints: ["POST /v1/ingest/cairn"]
7001
7017
  }),
7018
+ define({
7019
+ name: "plune_cases_generate",
7020
+ title: "Generate cases from pasted text",
7021
+ 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.",
7022
+ input: import_zod5.z.strictObject({ projectId, ...caseGenerationBody, dry_run: dryRun }),
7023
+ output: writeOutput,
7024
+ write: true,
7025
+ visibility: "core",
7026
+ endpoints: ["POST /v1/projects/:id/case-generations"]
7027
+ }),
7028
+ define({
7029
+ name: "plune_cases_generation_get",
7030
+ title: "Read a case generation",
7031
+ 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.",
7032
+ input: import_zod5.z.strictObject({ projectId, generationId: id("The id the start answered with") }),
7033
+ output: import_zod5.z.object({ generation: entity }),
7034
+ write: false,
7035
+ visibility: "core",
7036
+ endpoints: ["GET /v1/projects/:id/case-generations/:generationId"]
7037
+ }),
7002
7038
  define({
7003
7039
  name: "plune_mission_create",
7004
7040
  title: "Create a mission",
@@ -7364,10 +7400,10 @@ function createPluneToolExecutor(opts) {
7364
7400
  ]);
7365
7401
  const projectList = asArray(asRecord(projects.body)["projects"]);
7366
7402
  const only = projectList.length === 1 ? asRecord(projectList[0])["id"] : void 0;
7367
- const projectId = opts.projectId ?? (typeof only === "string" ? only : null);
7403
+ const projectId2 = opts.projectId ?? (typeof only === "string" ? only : null);
7368
7404
  const total = asRecord(cases.body)["total"];
7369
7405
  return {
7370
- projectId,
7406
+ projectId: projectId2,
7371
7407
  projects: projectList,
7372
7408
  suites: asArray(asRecord(suites.body)["suites"]),
7373
7409
  cases: { total: typeof total === "number" ? total : 0 },
@@ -7436,6 +7472,18 @@ function createPluneToolExecutor(opts) {
7436
7472
  const proposed = summary["proposed"];
7437
7473
  return { dryRun: false, requested: cases.length, applied: typeof proposed === "number" ? proposed : 0, report: summary };
7438
7474
  },
7475
+ async plune_cases_generate(i) {
7476
+ const { dry_run, projectId: projectId2, ...body } = i;
7477
+ const project = projectId2 ?? opts.projectId;
7478
+ if (project === void 0) return fail("invalid_input", NO_PROJECT);
7479
+ return write({ method: "POST", path: path19("/v1/projects/:id/case-generations", project), body: withoutUndefined(body) }, dry_run);
7480
+ },
7481
+ async plune_cases_generation_get(i) {
7482
+ const project = i["projectId"] ?? opts.projectId;
7483
+ if (project === void 0) return fail("invalid_input", NO_PROJECT);
7484
+ const res = await call({ method: "GET", path: path19("/v1/projects/:id/case-generations/:generationId", project, str2(i["generationId"])) });
7485
+ return { generation: asRecord(res.body) };
7486
+ },
7439
7487
  async plune_mission_create(i) {
7440
7488
  const { dry_run, ...body } = i;
7441
7489
  return write({ method: "POST", path: "/v1/missions", body: withoutUndefined(body) }, dry_run);
@@ -7579,7 +7627,7 @@ function createPluneToolExecutor(opts) {
7579
7627
  }
7580
7628
  };
7581
7629
  }
7582
- var import_node_crypto5, CONFIRM_TTL_MS, READ_ONLY_REFUSAL, readScopeMessage, fail, zodMessage, path19, withoutUndefined, asRecord, asArray, cursorOf, rawWrites;
7630
+ var import_node_crypto5, CONFIRM_TTL_MS, READ_ONLY_REFUSAL, NO_PROJECT, readScopeMessage, fail, zodMessage, path19, withoutUndefined, asRecord, asArray, cursorOf, rawWrites;
7583
7631
  var init_execute = __esm({
7584
7632
  "src/mcp-tools/execute.ts"() {
7585
7633
  "use strict";
@@ -7590,6 +7638,7 @@ var init_execute = __esm({
7590
7638
  init_schemas();
7591
7639
  CONFIRM_TTL_MS = 5 * 60 * 1e3;
7592
7640
  READ_ONLY_REFUSAL = "read-only token";
7641
+ NO_PROJECT = "projectId is needed: the server was started without a project (--project or PLUNE_PROJECT). plune_project_context lists the projects.";
7593
7642
  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
7643
  fail = (code, message, extra = {}) => ({
7595
7644
  ok: false,
@@ -7704,13 +7753,13 @@ async function handleMcpServe(flags, deps) {
7704
7753
  const token = (deps.loadToken ?? loadToken)();
7705
7754
  if (token === null) throw new NotLoggedInError();
7706
7755
  const apiUrl = resolveApiUrl(deps.apiUrl);
7707
- const projectId = flags.project?.trim() || process.env["PLUNE_PROJECT"]?.trim() || void 0;
7756
+ const projectId2 = flags.project?.trim() || process.env["PLUNE_PROJECT"]?.trim() || void 0;
7708
7757
  const serve = deps.serve ?? (await Promise.resolve().then(() => (init_server(), server_exports))).serveStdio;
7709
7758
  await serve({
7710
7759
  token,
7711
7760
  apiUrl,
7712
7761
  version: deps.version,
7713
- ...projectId !== void 0 ? { projectId } : {},
7762
+ ...projectId2 !== void 0 ? { projectId: projectId2 } : {},
7714
7763
  ...flags.readOnly === true ? { scope: "read" } : {}
7715
7764
  });
7716
7765
  }
@@ -8135,7 +8184,7 @@ function readMarkdown(cwd, file) {
8135
8184
  if (text2.trim() === "") usage(`${target} is empty.`);
8136
8185
  return text2;
8137
8186
  }
8138
- function refusal(out, apiUrl, projectId) {
8187
+ function refusal(out, apiUrl, projectId2) {
8139
8188
  const d = out.detail;
8140
8189
  const said = d === "" ? "" : ` (${d})`;
8141
8190
  switch (out.status) {
@@ -8146,7 +8195,7 @@ function refusal(out, apiUrl, projectId) {
8146
8195
  case 403:
8147
8196
  return new RequirementsImportError(`The platform refused: ${d || "read-only member"}. Importing needs an owner or a member of the project.`, 1);
8148
8197
  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);
8198
+ 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
8199
  case 409:
8151
8200
  return new RequirementsImportError(`A requirement key was taken by another change while the import ran. Nothing was written; run it again${said}.`, 1);
8152
8201
  case 413:
@@ -8176,15 +8225,15 @@ async function handleRequirementsImport(deps) {
8176
8225
  const token = (deps.loadToken ?? loadToken)();
8177
8226
  if (token === null) throw new RequirementsImportError('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.', 2);
8178
8227
  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`;
8228
+ const projectId2 = await resolveProject(client, deps.project, apiUrl);
8229
+ const base = `/v1/projects/${encodeURIComponent(projectId2)}/requirements/import`;
8181
8230
  if (mapping === null) {
8182
8231
  const out2 = await client.post(`${base}/discover`, { markdown });
8183
- if (!out2.ok) throw refusal(out2, apiUrl, projectId);
8232
+ if (!out2.ok) throw refusal(out2, apiUrl, projectId2);
8184
8233
  return { step: "discover", result: out2.body };
8185
8234
  }
8186
8235
  const out = await client.post(base, { markdown, dryRun: deps.yes !== true, ...mapping });
8187
- if (!out.ok) throw refusal(out, apiUrl, projectId);
8236
+ if (!out.ok) throw refusal(out, apiUrl, projectId2);
8188
8237
  return { step: "import", report: out.body };
8189
8238
  }
8190
8239
  function formatDiscover(file, r) {
@@ -8340,15 +8389,15 @@ async function handleRequirementsLinks(deps) {
8340
8389
  const token = (deps.loadToken ?? loadToken)();
8341
8390
  if (token === null) throw new RequirementsImportError('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.', 2);
8342
8391
  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`;
8392
+ const projectId2 = await resolveProject(client, deps.project, apiUrl);
8393
+ const base = `/v1/projects/${encodeURIComponent(projectId2)}/requirements/import`;
8345
8394
  if (mapping === null) {
8346
8395
  const out2 = await client.post(`${base}/discover`, { markdown });
8347
- if (!out2.ok) throw refusal(out2, apiUrl, projectId);
8396
+ if (!out2.ok) throw refusal(out2, apiUrl, projectId2);
8348
8397
  return { step: "discover", result: out2.body };
8349
8398
  }
8350
8399
  const out = await client.post(`${base}/links`, { markdown, dryRun: deps.yes !== true, ...mapping });
8351
- if (!out.ok) throw refusal(out, apiUrl, projectId);
8400
+ if (!out.ok) throw refusal(out, apiUrl, projectId2);
8352
8401
  return { step: "links", report: out.body };
8353
8402
  }
8354
8403
  function formatLinksDiscover(file, r) {
@@ -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",
@@ -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",
@@ -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.2",
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",