@meltstudio/meltctl 4.154.2 → 4.155.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +331 -35
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var CLI_VERSION;
14
14
  var init_version = __esm({
15
15
  "src/utils/version.ts"() {
16
16
  "use strict";
17
- CLI_VERSION = "4.154.2";
17
+ CLI_VERSION = "4.155.0";
18
18
  }
19
19
  });
20
20
 
@@ -663,6 +663,8 @@ function createProjectsResource(config) {
663
663
  const params = new URLSearchParams();
664
664
  if (opts?.activeOnly)
665
665
  params.set("activeOnly", "true");
666
+ if (opts?.includeInternal)
667
+ params.set("includeInternal", "true");
666
668
  const path9 = `/projects${params.toString() ? `?${params}` : ""}`;
667
669
  const { data, status } = await apiFetch(config, path9);
668
670
  if (status === 403)
@@ -942,8 +944,19 @@ function unwrap(label, response, okStatus = 200) {
942
944
  function createPmResource(config) {
943
945
  return {
944
946
  // ─── Features ─────────────────────────────────────────────────────────
945
- async listFeatures(projectId) {
946
- const res = await apiFetch(config, `/pm/features?projectId=${projectId}`);
947
+ async listFeatures(projectId, filters) {
948
+ const params = new URLSearchParams({ projectId: String(projectId) });
949
+ if (filters?.phaseId)
950
+ params.set("phaseId", filters.phaseId);
951
+ if (filters?.status)
952
+ params.set("status", filters.status);
953
+ if (filters?.category)
954
+ params.set("category", filters.category);
955
+ if (filters?.currentStage)
956
+ params.set("currentStage", filters.currentStage);
957
+ if (filters?.targetStage)
958
+ params.set("targetStage", filters.targetStage);
959
+ const res = await apiFetch(config, `/pm/features?${params}`);
947
960
  return unwrap("list features", res);
948
961
  },
949
962
  async getFeature(id) {
@@ -953,10 +966,28 @@ function createPmResource(config) {
953
966
  async createFeature(input3) {
954
967
  const res = await apiFetch(config, `/pm/features`, {
955
968
  method: "POST",
956
- body: JSON.stringify(input3)
969
+ body: JSON.stringify(normalizeCreateFeatureInput(input3))
957
970
  });
958
971
  return unwrap("create feature", res, 201);
959
972
  },
973
+ /**
974
+ * Bulk feature create — accepts an array of inputs and returns one
975
+ * result per item with `success: true | false` so partial failures
976
+ * don't sink the whole batch. Each item is wrapped in its own
977
+ * transaction (feature + phase memberships if any). Built for
978
+ * one-shot migrations of an existing roadmap into meltctl.
979
+ *
980
+ * Status code: 201 when every item succeeded; 207 (multi-status)
981
+ * when at least one failed. The unwrap layer accepts both.
982
+ */
983
+ async createFeaturesBatch(inputs) {
984
+ const res = await apiFetch(config, `/pm/features/batch`, {
985
+ method: "POST",
986
+ body: JSON.stringify({ features: inputs.map(normalizeCreateFeatureInput) })
987
+ });
988
+ const body = unwrap("create features batch", res, [201, 207]);
989
+ return body.results;
990
+ },
960
991
  async updateFeature(id, input3) {
961
992
  const res = await apiFetch(config, `/pm/features/${id}`, {
962
993
  method: "PATCH",
@@ -985,14 +1016,23 @@ function createPmResource(config) {
985
1016
  async createPhase(input3) {
986
1017
  const res = await apiFetch(config, `/pm/phases`, {
987
1018
  method: "POST",
988
- body: JSON.stringify(input3)
1019
+ body: JSON.stringify(normalizeCreatePhaseInput(input3))
989
1020
  });
990
1021
  return unwrap("create phase", res, 201);
991
1022
  },
1023
+ /** Bulk phase create — same shape as createFeaturesBatch. */
1024
+ async createPhasesBatch(inputs) {
1025
+ const res = await apiFetch(config, `/pm/phases/batch`, {
1026
+ method: "POST",
1027
+ body: JSON.stringify({ phases: inputs.map(normalizeCreatePhaseInput) })
1028
+ });
1029
+ const body = unwrap("create phases batch", res, [201, 207]);
1030
+ return body.results;
1031
+ },
992
1032
  async updatePhase(id, input3) {
993
1033
  const res = await apiFetch(config, `/pm/phases/${id}`, {
994
1034
  method: "PATCH",
995
- body: JSON.stringify(input3)
1035
+ body: JSON.stringify(normalizeUpdatePhaseInput(input3))
996
1036
  });
997
1037
  return unwrap("update phase", res);
998
1038
  },
@@ -1132,6 +1172,31 @@ function createPmResource(config) {
1132
1172
  }
1133
1173
  };
1134
1174
  }
1175
+ function toIsoOrNull(value) {
1176
+ if (value === void 0)
1177
+ return void 0;
1178
+ if (value === null)
1179
+ return null;
1180
+ return value instanceof Date ? value.toISOString() : value;
1181
+ }
1182
+ function normalizeCreateFeatureInput(input3) {
1183
+ if (input3.createdAt === void 0)
1184
+ return input3;
1185
+ return { ...input3, createdAt: toIsoOrNull(input3.createdAt) ?? void 0 };
1186
+ }
1187
+ function normalizeCreatePhaseInput(input3) {
1188
+ const out = { ...input3 };
1189
+ if (input3.createdAt !== void 0)
1190
+ out.createdAt = toIsoOrNull(input3.createdAt) ?? void 0;
1191
+ if (input3.closedAt !== void 0)
1192
+ out.closedAt = toIsoOrNull(input3.closedAt);
1193
+ return out;
1194
+ }
1195
+ function normalizeUpdatePhaseInput(input3) {
1196
+ if (input3.closedAt === void 0)
1197
+ return input3;
1198
+ return { ...input3, closedAt: toIsoOrNull(input3.closedAt) };
1199
+ }
1135
1200
 
1136
1201
  // ../sdk/dist/resources/slack.js
1137
1202
  function createSlackResource(config) {
@@ -3535,6 +3600,8 @@ function createProjectsResource2(config) {
3535
3600
  const params = new URLSearchParams();
3536
3601
  if (opts?.activeOnly)
3537
3602
  params.set("activeOnly", "true");
3603
+ if (opts?.includeInternal)
3604
+ params.set("includeInternal", "true");
3538
3605
  const path22 = `/projects${params.toString() ? `?${params}` : ""}`;
3539
3606
  const { data, status } = await apiFetch2(config, path22);
3540
3607
  if (status === 403)
@@ -3806,8 +3873,19 @@ function unwrap2(label, response, okStatus = 200) {
3806
3873
  function createPmResource2(config) {
3807
3874
  return {
3808
3875
  // ─── Features ─────────────────────────────────────────────────────────
3809
- async listFeatures(projectId) {
3810
- const res = await apiFetch2(config, `/pm/features?projectId=${projectId}`);
3876
+ async listFeatures(projectId, filters) {
3877
+ const params = new URLSearchParams({ projectId: String(projectId) });
3878
+ if (filters?.phaseId)
3879
+ params.set("phaseId", filters.phaseId);
3880
+ if (filters?.status)
3881
+ params.set("status", filters.status);
3882
+ if (filters?.category)
3883
+ params.set("category", filters.category);
3884
+ if (filters?.currentStage)
3885
+ params.set("currentStage", filters.currentStage);
3886
+ if (filters?.targetStage)
3887
+ params.set("targetStage", filters.targetStage);
3888
+ const res = await apiFetch2(config, `/pm/features?${params}`);
3811
3889
  return unwrap2("list features", res);
3812
3890
  },
3813
3891
  async getFeature(id) {
@@ -3817,10 +3895,28 @@ function createPmResource2(config) {
3817
3895
  async createFeature(input3) {
3818
3896
  const res = await apiFetch2(config, `/pm/features`, {
3819
3897
  method: "POST",
3820
- body: JSON.stringify(input3)
3898
+ body: JSON.stringify(normalizeCreateFeatureInput2(input3))
3821
3899
  });
3822
3900
  return unwrap2("create feature", res, 201);
3823
3901
  },
3902
+ /**
3903
+ * Bulk feature create — accepts an array of inputs and returns one
3904
+ * result per item with `success: true | false` so partial failures
3905
+ * don't sink the whole batch. Each item is wrapped in its own
3906
+ * transaction (feature + phase memberships if any). Built for
3907
+ * one-shot migrations of an existing roadmap into meltctl.
3908
+ *
3909
+ * Status code: 201 when every item succeeded; 207 (multi-status)
3910
+ * when at least one failed. The unwrap layer accepts both.
3911
+ */
3912
+ async createFeaturesBatch(inputs) {
3913
+ const res = await apiFetch2(config, `/pm/features/batch`, {
3914
+ method: "POST",
3915
+ body: JSON.stringify({ features: inputs.map(normalizeCreateFeatureInput2) })
3916
+ });
3917
+ const body = unwrap2("create features batch", res, [201, 207]);
3918
+ return body.results;
3919
+ },
3824
3920
  async updateFeature(id, input3) {
3825
3921
  const res = await apiFetch2(config, `/pm/features/${id}`, {
3826
3922
  method: "PATCH",
@@ -3849,14 +3945,23 @@ function createPmResource2(config) {
3849
3945
  async createPhase(input3) {
3850
3946
  const res = await apiFetch2(config, `/pm/phases`, {
3851
3947
  method: "POST",
3852
- body: JSON.stringify(input3)
3948
+ body: JSON.stringify(normalizeCreatePhaseInput2(input3))
3853
3949
  });
3854
3950
  return unwrap2("create phase", res, 201);
3855
3951
  },
3952
+ /** Bulk phase create — same shape as createFeaturesBatch. */
3953
+ async createPhasesBatch(inputs) {
3954
+ const res = await apiFetch2(config, `/pm/phases/batch`, {
3955
+ method: "POST",
3956
+ body: JSON.stringify({ phases: inputs.map(normalizeCreatePhaseInput2) })
3957
+ });
3958
+ const body = unwrap2("create phases batch", res, [201, 207]);
3959
+ return body.results;
3960
+ },
3856
3961
  async updatePhase(id, input3) {
3857
3962
  const res = await apiFetch2(config, `/pm/phases/${id}`, {
3858
3963
  method: "PATCH",
3859
- body: JSON.stringify(input3)
3964
+ body: JSON.stringify(normalizeUpdatePhaseInput2(input3))
3860
3965
  });
3861
3966
  return unwrap2("update phase", res);
3862
3967
  },
@@ -3996,6 +4101,31 @@ function createPmResource2(config) {
3996
4101
  }
3997
4102
  };
3998
4103
  }
4104
+ function toIsoOrNull2(value) {
4105
+ if (value === void 0)
4106
+ return void 0;
4107
+ if (value === null)
4108
+ return null;
4109
+ return value instanceof Date ? value.toISOString() : value;
4110
+ }
4111
+ function normalizeCreateFeatureInput2(input3) {
4112
+ if (input3.createdAt === void 0)
4113
+ return input3;
4114
+ return { ...input3, createdAt: toIsoOrNull2(input3.createdAt) ?? void 0 };
4115
+ }
4116
+ function normalizeCreatePhaseInput2(input3) {
4117
+ const out = { ...input3 };
4118
+ if (input3.createdAt !== void 0)
4119
+ out.createdAt = toIsoOrNull2(input3.createdAt) ?? void 0;
4120
+ if (input3.closedAt !== void 0)
4121
+ out.closedAt = toIsoOrNull2(input3.closedAt);
4122
+ return out;
4123
+ }
4124
+ function normalizeUpdatePhaseInput2(input3) {
4125
+ if (input3.closedAt === void 0)
4126
+ return input3;
4127
+ return { ...input3, closedAt: toIsoOrNull2(input3.closedAt) };
4128
+ }
3999
4129
  function createSlackResource2(config) {
4000
4130
  return {
4001
4131
  async listChannels() {
@@ -4560,36 +4690,88 @@ function withClientArgs(getClient2, handler) {
4560
4690
  }
4561
4691
  };
4562
4692
  }
4563
- async function listProjects(client) {
4564
- return safe(() => client.projects.list({ activeOnly: true }));
4693
+ async function listProjects(client, input3) {
4694
+ const hasPM = input3.hasPM ?? true;
4695
+ const includeInternal = input3.includeInternal ?? false;
4696
+ return safe(() => client.projects.list({ activeOnly: hasPM, includeInternal }));
4565
4697
  }
4566
4698
  async function getProjectSettings(client, input3) {
4567
4699
  return safe(() => client.pm.getProjectSettings(input3.projectId));
4568
4700
  }
4701
+ async function updateProjectSettings(client, input3) {
4702
+ const { projectId, ...patch } = input3;
4703
+ return safe(
4704
+ () => client.pm.updateProjectSettings(projectId, patch)
4705
+ );
4706
+ }
4569
4707
  async function getProjectContext(client, input3) {
4570
4708
  return safe(() => client.pm.getProjectContext(input3.projectId));
4571
4709
  }
4710
+ var PROJECT_STAGE_VALUES = ["prototype", "early_revenue", "growth", "scale"];
4572
4711
  function registerProjectTools(server, getClient2) {
4573
4712
  server.registerTool(
4574
4713
  "list_projects",
4575
4714
  {
4576
4715
  title: "List projects",
4577
- description: "Lists every actively-managed Melt project the authenticated user can see \u2014 i.e. projects with a project manager assigned. Use this to resolve a project's name to its numeric `projectId` before calling any other tool. Returns id, name, client, project manager, and repo count per project. Projects without a PM are unmanaged and intentionally excluded; create_feature / assign_feature_to_phase against an unmanaged project will succeed but is rarely intended.",
4578
- inputSchema: {}
4716
+ description: "Lists Melt projects the authenticated user can see. Defaults: hasPM=true (only actively-managed projects), includeInternal=false (Melt's own internal projects hidden). Use this to resolve a project's name to its numeric `projectId` before calling any other tool. Returns id, name, client, project manager, and repo count per project.",
4717
+ inputSchema: {
4718
+ hasPM: z22.boolean().optional().describe(
4719
+ "Default true. When true, only projects with a project manager assigned are returned (recommended for AI agent flows). Set false to include unmanaged projects."
4720
+ ),
4721
+ includeInternal: z22.boolean().optional().describe(
4722
+ "Default false. When false, Melt's own internal projects are hidden. Set true to include them."
4723
+ )
4724
+ }
4579
4725
  },
4580
- withClient(getClient2, listProjects)
4726
+ withClientArgs(getClient2, listProjects)
4581
4727
  );
4582
4728
  server.registerTool(
4583
4729
  "get_project_settings",
4584
4730
  {
4585
4731
  title: "Get project settings",
4586
- description: "Returns the project's PM-skills settings: cadence (weekly/biweekly/monthly), start_day, demo_day, team_size, velocity_override, Linear workspace, Notion URLs, code repo path, tech-support contact, default language, project glossary. Returns sensible defaults if no settings row exists yet (cadence=weekly, start_day=Monday, demo_day=Friday, defaultLanguage=English).",
4732
+ description: 'Returns the project\'s PM-skills settings: cadence (weekly/biweekly/monthly), start_day, demo_day, team_size, velocity_override, Linear workspace, Notion URLs, code repo path, tech-support contact, default language, project glossary, project stage. Includes a `warnings` array flagging settings whose absence breaks downstream behavior (e.g., "calculator: missing teamSize"). Returns sensible defaults if no settings row exists yet (cadence=weekly, start_day=Monday, demo_day=Friday, defaultLanguage=English).',
4587
4733
  inputSchema: {
4588
4734
  projectId: z22.number().int().positive().describe("Strapi project id from list_projects.")
4589
4735
  }
4590
4736
  },
4591
4737
  withClientArgs(getClient2, getProjectSettings)
4592
4738
  );
4739
+ server.registerTool(
4740
+ "update_project_settings",
4741
+ {
4742
+ title: "Update project settings",
4743
+ description: "Patches PM-skills settings for a project. Pass only the fields you want to change. Use cases: setting teamSize after a roadmap migration so the maturity-jumps calculator renders, setting projectStage so audit calibration uses the right baseline, wiring notionProjectProfileUrl so risks/feedback route to the right Notion page. The PM should review the proposed change before this is called \u2014 this is a write operation. Pass null to clear a nullable field.",
4744
+ inputSchema: {
4745
+ projectId: z22.number().int().positive().describe("Strapi project id from list_projects."),
4746
+ cadence: z22.enum(["weekly", "biweekly", "monthly"]).optional().describe("Sprint cadence."),
4747
+ startDay: z22.string().min(1).optional().describe('Sprint start day, e.g. "Monday".'),
4748
+ demoDay: z22.string().min(1).optional().describe('Demo day, e.g. "Friday".'),
4749
+ teamSize: z22.number().int().nonnegative().nullable().optional().describe(
4750
+ "Active developer count for the maturity-jumps calculator. Pass null to clear (calculator falls back to 1)."
4751
+ ),
4752
+ velocityOverride: z22.number().positive().nullable().optional().describe(
4753
+ "Project-specific velocity override (stories/week/dev). Wins over the company baseline when set. Pass null to clear."
4754
+ ),
4755
+ linearWorkspace: z22.string().nullable().optional().describe('Linear workspace slug, e.g. "meltstudio". Pass null to clear.'),
4756
+ notionCallLogUrl: z22.string().url().nullable().optional().describe("Notion URL for the team's call log. Pass null to clear."),
4757
+ notionProjectProfileUrl: z22.string().url().nullable().optional().describe(
4758
+ "Notion URL for the project profile page (Risks + Feedback sections live there). Required for intake routing of risks/feedback. Pass null to clear."
4759
+ ),
4760
+ codeRepoPath: z22.string().nullable().optional().describe(
4761
+ "Local path to the project's code repo. When set, intake skills can investigate code inline; when null, they create tech-investigation requests instead."
4762
+ ),
4763
+ techSupportContact: z22.string().nullable().optional().describe("Email or handle of the project's tech-support contact. Pass null to clear."),
4764
+ defaultLanguage: z22.string().min(1).optional().describe("Default language for client-facing artifacts (recap emails, etc)."),
4765
+ projectGlossary: z22.string().nullable().optional().describe(
4766
+ "Free-text project glossary surfaced to skills as context. Pass null to clear."
4767
+ ),
4768
+ projectStage: z22.enum(PROJECT_STAGE_VALUES).nullable().optional().describe(
4769
+ "Canonical PM-owned project stage that drives audit calibration: prototype | early_revenue | growth | scale. The server stamps projectStageSetBy + projectStageSetAt automatically. Pass null to clear."
4770
+ )
4771
+ }
4772
+ },
4773
+ withClientArgs(getClient2, updateProjectSettings)
4774
+ );
4593
4775
  server.registerTool(
4594
4776
  "get_project_context",
4595
4777
  {
@@ -4602,19 +4784,26 @@ function registerProjectTools(server, getClient2) {
4602
4784
  withClientArgs(getClient2, getProjectContext)
4603
4785
  );
4604
4786
  }
4787
+ var SHAPE_VALUES = ["crawling", "walking", "running"];
4605
4788
  var SHAPE_DESC = "crawling | walking | running. Target shape is intent (PM sets when creating); current shape is computed from feature stages.";
4606
4789
  async function listPhases(client, input3) {
4607
4790
  return safe(() => client.pm.listPhases(input3.projectId, input3.includeClosed ?? false));
4608
4791
  }
4609
4792
  async function createPhase(client, input3) {
4610
- return safe(
4611
- () => client.pm.createPhase({
4612
- projectId: input3.projectId,
4613
- name: input3.name,
4614
- targetShape: input3.targetShape,
4615
- ...input3.isPrimary !== void 0 ? { isPrimary: input3.isPrimary } : {}
4616
- })
4617
- );
4793
+ return safe(() => client.pm.createPhase(input3));
4794
+ }
4795
+ async function createPhasesBatch(client, input3) {
4796
+ return safe(() => client.pm.createPhasesBatch(input3.phases));
4797
+ }
4798
+ async function updatePhase(client, input3) {
4799
+ const { id, ...patch } = input3;
4800
+ return safe(() => client.pm.updatePhase(id, patch));
4801
+ }
4802
+ async function deletePhase(client, input3) {
4803
+ return safe(async () => {
4804
+ await client.pm.deletePhase(input3.id);
4805
+ return { ok: true, id: input3.id };
4806
+ });
4618
4807
  }
4619
4808
  async function assignFeatureToPhase(client, input3) {
4620
4809
  return safe(() => client.pm.assignFeature(input3.phaseId, input3.featureId));
@@ -4625,8 +4814,10 @@ async function unassignFeatureFromPhase(client, input3) {
4625
4814
  var createPhaseInputSchema = z3.object({
4626
4815
  projectId: z3.number().int().positive(),
4627
4816
  name: z3.string().min(1),
4628
- targetShape: z3.enum(["crawling", "walking", "running"]).default("walking"),
4629
- isPrimary: z3.boolean().optional()
4817
+ targetShape: z3.enum(SHAPE_VALUES).default("walking"),
4818
+ isPrimary: z3.boolean().optional(),
4819
+ createdAt: z3.string().datetime().optional(),
4820
+ closedAt: z3.string().datetime().nullable().optional()
4630
4821
  });
4631
4822
  function registerPhaseTools(server, getClient2) {
4632
4823
  server.registerTool(
@@ -4645,18 +4836,74 @@ function registerPhaseTools(server, getClient2) {
4645
4836
  "create_phase",
4646
4837
  {
4647
4838
  title: "Create phase",
4648
- description: "Creates a new phase on the project. The PM should review the proposed name + target shape before this is called \u2014 this is a write operation. If isPrimary=true, any existing primary phase on the project is automatically demoted (atomic). Use isPrimary sparingly; multi-active phases without changing primary is the safer default.",
4839
+ description: "Creates a new phase on the project. The PM should review the proposed name + target shape before this is called \u2014 this is a write operation. If isPrimary=true, any existing primary phase on the project is automatically demoted (atomic). Use isPrimary sparingly; multi-active phases without changing primary is the safer default. For roadmap migrations preserving historical timelines, pass createdAt and/or closedAt \u2014 a non-null closedAt also marks the phase isActive=false + isPrimary=false.",
4649
4840
  inputSchema: {
4650
4841
  projectId: z3.number().int().positive(),
4651
4842
  name: z3.string().min(1).describe('e.g. "MVP launch", "V2 features".'),
4652
- targetShape: z3.enum(["crawling", "walking", "running"]).default("walking").describe(SHAPE_DESC),
4843
+ targetShape: z3.enum(SHAPE_VALUES).default("walking").describe(SHAPE_DESC),
4653
4844
  isPrimary: z3.boolean().optional().describe(
4654
4845
  "Default false. Setting true demotes any existing primary phase. Only one primary per project."
4846
+ ),
4847
+ createdAt: z3.string().datetime().optional().describe(
4848
+ "ISO 8601 timestamp. Backdates createdAt \u2014 only set this when migrating an existing phase that has a real historical creation date."
4849
+ ),
4850
+ closedAt: z3.string().datetime().nullable().optional().describe(
4851
+ "ISO 8601 timestamp. When set, the phase is created as historical (isActive=false, isPrimary=false). Use to backfill phases that closed months ago."
4655
4852
  )
4656
4853
  }
4657
4854
  },
4658
4855
  withClientArgs(getClient2, createPhase)
4659
4856
  );
4857
+ server.registerTool(
4858
+ "create_phases_batch",
4859
+ {
4860
+ title: "Create phases batch",
4861
+ description: "Bulk phase create \u2014 accepts an array of inputs (max 100) and returns one result per input with success or error. Each item is wrapped in its own transaction. Built for one-shot migrations of an existing roadmap.",
4862
+ inputSchema: {
4863
+ phases: z3.array(createPhaseInputSchema).min(1).max(100).describe("Up to 100 phase inputs. Each accepts the same fields as create_phase.")
4864
+ }
4865
+ },
4866
+ withClientArgs(getClient2, createPhasesBatch)
4867
+ );
4868
+ server.registerTool(
4869
+ "update_phase",
4870
+ {
4871
+ title: "Update phase",
4872
+ description: "Patches fields on an existing phase. Pass only the fields you want to change. Common uses: rename, switch target shape, override or clear the computed current shape, set or clear primary, mark a phase closed/reopened. The PM should review the proposed change before this is called \u2014 this is a write operation. Setting closedAt non-null forces isActive=false + isPrimary=false; setting isActive=false also clears isPrimary.",
4873
+ inputSchema: {
4874
+ id: z3.string().uuid().describe("Phase id from list_phases or create_phase."),
4875
+ name: z3.string().min(1).optional(),
4876
+ targetShape: z3.enum(SHAPE_VALUES).optional().describe(SHAPE_DESC),
4877
+ currentShapeOverride: z3.enum(SHAPE_VALUES).nullable().optional().describe(
4878
+ "Force the phase's current shape regardless of feature distribution. Pass null to clear and resume computed-shape behavior."
4879
+ ),
4880
+ overrideReason: z3.string().nullable().optional().describe("Free-text reason for the shape override. Pass null to clear."),
4881
+ isPrimary: z3.boolean().optional().describe(
4882
+ "Promote this phase to primary. Server demotes any existing primary atomically."
4883
+ ),
4884
+ isActive: z3.boolean().optional().describe(
4885
+ "Set to false to deactivate (also clears isPrimary). Set to true to reopen a closed phase (closedAt stays unless you also clear it)."
4886
+ ),
4887
+ closedAt: z3.string().datetime().nullable().optional().describe(
4888
+ "ISO 8601 timestamp or null. When set non-null, forces isActive=false + isPrimary=false. When null, clears the closed-at stamp."
4889
+ )
4890
+ }
4891
+ },
4892
+ withClientArgs(getClient2, updatePhase)
4893
+ );
4894
+ server.registerTool(
4895
+ "delete_phase",
4896
+ {
4897
+ title: "Delete phase (DESTRUCTIVE \u2014 hard delete, no undo)",
4898
+ description: `\u26A0 DESTRUCTIVE. Hard-deletes the phase row from postgres. Not a soft archive \u2014 the row is gone and there is no recovery short of restoring the database from backup. The API refuses the delete if the phase still has features assigned (move or unassign the features first); this safety net prevents accidentally cascading away every phase membership in one call. Use ONLY for rolling back a bad insert during a migration or removing a phase created in error.
4899
+
4900
+ Mandatory caller behavior: present the phase's name + id + active/closed status + the count of any current feature memberships to the PM, get explicit confirmation ("yes, delete <name>"), then call. Never call without that confirmation. If you have any doubt, prefer closing the phase via update_phase (closedAt + isActive=false) instead \u2014 that preserves history.`,
4901
+ inputSchema: {
4902
+ id: z3.string().uuid().describe("Phase id from list_phases or create_phase.")
4903
+ }
4904
+ },
4905
+ withClientArgs(getClient2, deletePhase)
4906
+ );
4660
4907
  server.registerTool(
4661
4908
  "assign_feature_to_phase",
4662
4909
  {
@@ -4686,11 +4933,21 @@ var STAGE_VALUES = ["idea", "poc", "pt", "mv", "mk", "ma", "mbi"];
4686
4933
  var STAGE_DESC = "idea | poc | pt | mv | mk | ma | mbi. The 7 maturity stages from the workflow doc \u2014 pick the one that matches where the work *looks like* it is right now (or where it should land for target).";
4687
4934
  var PRD_PATH_DESC = "http(s) URL pointing to the PRD (Notion, Google Doc, etc.). Omit if there is no PRD yet \u2014 relative paths or plain text are rejected.";
4688
4935
  async function listFeatures(client, input3) {
4689
- return safe(() => client.pm.listFeatures(input3.projectId));
4936
+ const { projectId, ...filters } = input3;
4937
+ return safe(() => client.pm.listFeatures(projectId, filters));
4690
4938
  }
4691
4939
  async function createFeature(client, input3) {
4692
4940
  return safe(() => client.pm.createFeature(input3));
4693
4941
  }
4942
+ async function createFeaturesBatch(client, input3) {
4943
+ return safe(() => client.pm.createFeaturesBatch(input3.features));
4944
+ }
4945
+ async function deleteFeature(client, input3) {
4946
+ return safe(async () => {
4947
+ await client.pm.deleteFeature(input3.id);
4948
+ return { ok: true, id: input3.id };
4949
+ });
4950
+ }
4694
4951
  async function updateFeature(client, input3) {
4695
4952
  const { id, ...patch } = input3;
4696
4953
  return safe(() => client.pm.updateFeature(id, patch));
@@ -4704,7 +4961,10 @@ var createFeatureInputSchema = z4.object({
4704
4961
  currentStage: z4.enum(STAGE_VALUES).optional(),
4705
4962
  targetStage: z4.enum(STAGE_VALUES).optional(),
4706
4963
  status: z4.string().optional(),
4707
- clientDependencies: z4.string().optional()
4964
+ clientDependencies: z4.string().optional(),
4965
+ createdAt: z4.string().datetime().optional(),
4966
+ phaseId: z4.string().uuid().optional(),
4967
+ phaseIds: z4.array(z4.string().uuid()).optional()
4708
4968
  });
4709
4969
  var updateFeatureInputSchema = z4.object({
4710
4970
  id: z4.string().uuid(),
@@ -4723,9 +4983,14 @@ function registerFeatureTools(server, getClient2) {
4723
4983
  "list_features",
4724
4984
  {
4725
4985
  title: "List features",
4726
- description: "Lists every feature on the project. Each entry has id, name, category, description, current_stage, target_stage, status, prd_path, client_dependencies, and Linear epic url if synced.",
4986
+ description: "Lists features on the project. Each entry has id, name, category, description, current_stage, target_stage, status, prd_path, client_dependencies, and Linear epic url if synced. Optional filters narrow server-side: phaseId returns only features assigned to that phase; status / category / currentStage / targetStage filter on the matching column.",
4727
4987
  inputSchema: {
4728
- projectId: z4.number().int().positive()
4988
+ projectId: z4.number().int().positive(),
4989
+ phaseId: z4.string().uuid().optional().describe("Limit to features assigned to this phase (phase_features join)."),
4990
+ status: z4.string().optional().describe("Filter by status \u2014 typically not_started / in_progress / done."),
4991
+ category: z4.string().optional().describe("Filter by roadmap category, exact match."),
4992
+ currentStage: z4.enum(STAGE_VALUES).optional().describe("Filter by current_stage."),
4993
+ targetStage: z4.enum(STAGE_VALUES).optional().describe("Filter by target_stage.")
4729
4994
  }
4730
4995
  },
4731
4996
  withClientArgs(getClient2, listFeatures)
@@ -4734,7 +4999,7 @@ function registerFeatureTools(server, getClient2) {
4734
4999
  "create_feature",
4735
5000
  {
4736
5001
  title: "Create feature",
4737
- description: "Creates a new feature on the project. The PM should review the proposed fields before this is called \u2014 this is a write operation. After creation, use assign_feature_to_phase to attach it to one or more phases.",
5002
+ description: "Creates a new feature on the project. The PM should review the proposed fields before this is called \u2014 this is a write operation. Optionally pre-assigns the new feature to one or more phases via phaseId / phaseIds (atomic \u2014 feature + memberships land together or neither does). For roadmap migrations from older sources, pass createdAt to preserve the historical creation timestamp.",
4738
5003
  inputSchema: {
4739
5004
  projectId: z4.number().int().positive(),
4740
5005
  name: z4.string().min(1),
@@ -4744,11 +5009,42 @@ function registerFeatureTools(server, getClient2) {
4744
5009
  currentStage: z4.enum(STAGE_VALUES).optional().describe(`Default idea. ${STAGE_DESC}`),
4745
5010
  targetStage: z4.enum(STAGE_VALUES).optional().describe(`Default mv. ${STAGE_DESC}`),
4746
5011
  status: z4.string().optional().describe("Default not_started. Common values: not_started, in_progress, done."),
4747
- clientDependencies: z4.string().optional().describe("Free-text. What blockers from the client side, if any.")
5012
+ clientDependencies: z4.string().optional().describe("Free-text. What blockers from the client side, if any."),
5013
+ createdAt: z4.string().datetime().optional().describe(
5014
+ "ISO 8601 timestamp. Backdates the row's createdAt \u2014 only set this when migrating an existing roadmap that has a real historical creation date. Otherwise omit and the server stamps now()."
5015
+ ),
5016
+ phaseId: z4.string().uuid().optional().describe("Atomically assign the new feature to this phase. Same project required."),
5017
+ phaseIds: z4.array(z4.string().uuid()).optional().describe(
5018
+ "Atomically assign the new feature to multiple phases. Same project required for every phase."
5019
+ )
4748
5020
  }
4749
5021
  },
4750
5022
  withClientArgs(getClient2, createFeature)
4751
5023
  );
5024
+ server.registerTool(
5025
+ "create_features_batch",
5026
+ {
5027
+ title: "Create features batch",
5028
+ description: `Bulk feature create \u2014 accepts an array of inputs (max 200) and returns one result per input with success or error. Each item is wrapped in its own transaction (feature + phase memberships if any), so partial failures don't sink the whole batch. Built for one-shot migrations of an existing roadmap (Excel "Features List" sheets etc).`,
5029
+ inputSchema: {
5030
+ features: z4.array(createFeatureInputSchema).min(1).max(200).describe("Up to 200 feature inputs. Each accepts the same fields as create_feature.")
5031
+ }
5032
+ },
5033
+ withClientArgs(getClient2, createFeaturesBatch)
5034
+ );
5035
+ server.registerTool(
5036
+ "delete_feature",
5037
+ {
5038
+ title: "Delete feature (DESTRUCTIVE \u2014 hard delete, no undo)",
5039
+ description: `\u26A0 DESTRUCTIVE. Hard-deletes the feature row from postgres. Not a soft archive \u2014 the row is gone, the DB cascades all phase_features memberships so the feature disappears from every phase it was attached to, and there is no recovery short of restoring the database from backup. Use ONLY for rolling back a bad insert during a migration or removing a feature created in error.
5040
+
5041
+ Mandatory caller behavior: present the feature's name + id + every phase it currently belongs to to the PM, get explicit confirmation ("yes, delete <name>"), then call. Never call without that confirmation. If you have any doubt, prefer marking the feature's status to something like "cancelled" via update_feature instead \u2014 that preserves history.`,
5042
+ inputSchema: {
5043
+ id: z4.string().uuid().describe("Feature id from list_features or create_feature.")
5044
+ }
5045
+ },
5046
+ withClientArgs(getClient2, deleteFeature)
5047
+ );
4752
5048
  server.registerTool(
4753
5049
  "update_feature",
4754
5050
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meltstudio/meltctl",
3
- "version": "4.154.2",
3
+ "version": "4.155.0",
4
4
  "description": "AI-first development tools for teams - set up AGENTS.md, Claude Code, Cursor, and OpenCode standards",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",