bambu-printer-mcp 1.1.6 → 1.1.8

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/dist/index.js CHANGED
@@ -5,11 +5,13 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
5
5
  import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
6
6
  import dotenv from "dotenv";
7
7
  import fs from "fs";
8
+ import os from "node:os";
8
9
  import path from "path";
9
10
  import { createServer as createHttpServer } from "node:http";
10
11
  import { randomUUID } from "node:crypto";
11
12
  import { STLManipulator, SLICER_TYPES, normalizeSlicerType, } from "./stl/stl-manipulator.js";
12
13
  import { BambuNetworkBridge } from "./bambu-network-bridge.js";
14
+ import { BlenderMcpBridge } from "./blender-mcp-bridge.js";
13
15
  import { hasAmsMappingInput, normalizeAmsMappingObject, normalizeBridgeAmsTrayValue } from "./ams-mapping.js";
14
16
  import { analyze3MFAmsRequirements, analyze3MFPlateObjects, analyzeCollarCharm3MF, extractBambuTemplateSettings, getCollarCharmRolePolicy, parse3MF } from './3mf_parser.js';
15
17
  import { BambuImplementation } from "./printers/bambu.js";
@@ -18,7 +20,24 @@ const DEFAULT_HOST = process.env.BAMBU_PRINTER_HOST || process.env.PRINTER_HOST
18
20
  const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_PRINTER_SERIAL || process.env.BAMBU_SERIAL || "";
19
21
  const DEFAULT_BAMBU_TOKEN = process.env.BAMBU_PRINTER_ACCESS_TOKEN || process.env.BAMBU_TOKEN || "";
20
22
  const DEFAULT_BAMBU_DEV_ID = process.env.BAMBU_DEV_ID || DEFAULT_BAMBU_SERIAL;
21
- const TEMP_DIR = process.env.TEMP_DIR || path.join(process.cwd(), "temp");
23
+ // os.tmpdir(), not process.cwd() — a packaged Claude Desktop extension is
24
+ // spawned with a cwd we don't control (and may not be writable), so a
25
+ // cwd-relative default here can throw at module load via the mkdirSync
26
+ // below and kill the server before it ever opens the stdio transport.
27
+ const AUTOMATIC_TEMP_DIR = process.env.TEMP_DIR ? undefined : fs.mkdtempSync(path.join(os.tmpdir(), "bambu-printer-mcp-"));
28
+ const TEMP_DIR = process.env.TEMP_DIR || AUTOMATIC_TEMP_DIR;
29
+ if (AUTOMATIC_TEMP_DIR) {
30
+ // The exit event also covers startup failures and normal event-loop exit.
31
+ // Only the directory created above belongs to us; explicit TEMP_DIR is retained.
32
+ process.once("exit", () => {
33
+ try {
34
+ fs.rmSync(AUTOMATIC_TEMP_DIR, { recursive: true, force: true });
35
+ }
36
+ catch (error) {
37
+ console.error("Unable to remove the server's temporary directory:", error);
38
+ }
39
+ });
40
+ }
22
41
  // Printer model and bed type
23
42
  const DEFAULT_BAMBU_MODEL = process.env.BAMBU_PRINTER_MODEL?.trim().toLowerCase() ||
24
43
  process.env.BAMBU_MODEL?.trim().toLowerCase() ||
@@ -743,10 +762,12 @@ if (!fs.existsSync(TEMP_DIR)) {
743
762
  }
744
763
  class BambuPrinterMCPServer {
745
764
  constructor() {
765
+ this.blender = new BlenderMcpBridge();
766
+ this.shuttingDown = false;
746
767
  this.runtimeConfig = readRuntimeConfig();
747
768
  this.server = new Server({
748
769
  name: "bambu-printer-mcp",
749
- version: "1.0.0"
770
+ version: JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version
750
771
  }, {
751
772
  capabilities: {
752
773
  resources: {},
@@ -2140,28 +2161,62 @@ class BambuPrinterMCPServer {
2140
2161
  required: ["stl_path"]
2141
2162
  }
2142
2163
  },
2164
+ {
2165
+ name: "blender_mcp_status",
2166
+ description: "Inspect Blender MCP configuration or connect and discover the remote server's tools and schemas. Connecting does not edit the scene; use get_scene_info through blender_mcp_call to check the Blender addon.",
2167
+ inputSchema: {
2168
+ type: "object",
2169
+ properties: {
2170
+ connect: { type: "boolean", description: "Initialize the configured stdio MCP server and discover its tools (default false)." },
2171
+ timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total connection and discovery deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." }
2172
+ },
2173
+ additionalProperties: false
2174
+ }
2175
+ },
2176
+ {
2177
+ name: "blender_mcp_call",
2178
+ description: "Call a discovered tool on the configured Blender MCP server, preserving its full MCP content and errors. Discover tool schemas with blender_mcp_status first; execute_blender_code accepts Python code and user_prompt. Calls can modify the active Blender scene and are never automatically retried.",
2179
+ inputSchema: {
2180
+ type: "object",
2181
+ properties: {
2182
+ tool_name: { type: "string", description: "Exact name advertised by Blender MCP, such as get_scene_info or execute_blender_code." },
2183
+ arguments: { type: "object", description: "Arguments matching the remote tool's discovered input schema. Preserve the user's own words in user_prompt when the remote tool requests it." },
2184
+ timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total connection, discovery, and tool deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." }
2185
+ },
2186
+ required: ["tool_name"],
2187
+ additionalProperties: false
2188
+ }
2189
+ },
2143
2190
  {
2144
2191
  name: "blender_mcp_edit_model",
2145
- description: "Send STL-edit instructions to a Blender MCP bridge command for advanced model edits",
2192
+ description: "Import, edit, and export a local STL through standard Blender MCP with verified output and existing scene objects preserved. Requires a shared local filesystem and Blender Object Mode. Also supports a separately configured legacy executable bridge.",
2146
2193
  inputSchema: {
2147
2194
  type: "object",
2148
2195
  properties: {
2149
2196
  stl_path: { type: "string", description: "Path to the local STL file" },
2150
2197
  operations: {
2151
2198
  type: "array",
2152
- description: "Ordered edit operations for Blender (e.g. remesh, boolean, decimate)",
2199
+ description: "Ordered operations: decimate:<ratio greater than 0 and at most 1>, remesh:<positive voxel size in STL units>, boolean_union:<STL path>. Legacy custom bridges define their own operations.",
2200
+ minItems: 1,
2201
+ maxItems: 64,
2153
2202
  items: { type: "string" }
2154
2203
  },
2155
- bridge_command: { type: "string", description: "Override command for invoking Blender MCP bridge. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
2156
- execute: { type: "boolean", description: "Execute bridge command (true) or return payload only (false)" }
2204
+ output_path: { type: "string", description: "Required for standard MCP previews and execution: new local STL output path whose parent exists. Existing files are never overwritten. Optional for legacy-only bridge configuration." },
2205
+ user_prompt: { type: "string", description: "The user's own words describing the edit, passed unchanged to Blender MCP." },
2206
+ timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total Blender request deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." },
2207
+ bridge_command: { type: "string", description: "Legacy custom bridge executable override, not a standard MCP command. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
2208
+ execute: { type: "boolean", description: "Apply edits and export (true) or validate and return the prepared request without connecting (false, default)." }
2157
2209
  },
2158
- required: ["stl_path", "operations"]
2210
+ required: process.env.BLENDER_MCP_COMMAND?.trim()
2211
+ ? ["stl_path", "operations", "output_path"]
2212
+ : ["stl_path", "operations"],
2213
+ additionalProperties: false
2159
2214
  }
2160
2215
  }
2161
2216
  ]
2162
2217
  };
2163
2218
  });
2164
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
2219
+ this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
2165
2220
  const { name, arguments: args } = request.params;
2166
2221
  const host = String(args?.host || DEFAULT_HOST);
2167
2222
  const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
@@ -2583,45 +2638,37 @@ class BambuPrinterMCPServer {
2583
2638
  throw new Error("plate_index must be a non-negative integer.");
2584
2639
  }
2585
2640
  let threeMFPath = String(args.three_mf_path);
2586
- // Auto-slice if 3MF has no gcode
2587
- try {
2588
- const JSZip = (await import('jszip')).default;
2589
- const zipData = fs.readFileSync(threeMFPath);
2590
- const zip = await JSZip.loadAsync(zipData);
2591
- const hasGcode = Object.keys(zip.files).some(f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode'));
2592
- if (!hasGcode) {
2593
- if (printBedType === "supertack_plate") {
2594
- throw new Error('BambuStudio CLI SuperTack bed type is not verified; use a pre-sliced 3MF for SuperTack or choose textured_plate, cool_plate, engineering_plate, or hot_plate.');
2595
- }
2596
- console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
2597
- const autoSliceOptions = {
2598
- uptodate: true,
2599
- ensureOnBed: true,
2600
- minSave: true,
2601
- skipModifiedGcodes: true,
2602
- bedType: printBedType,
2603
- };
2604
- if (!explicitSlicerProfile) {
2605
- try {
2606
- const liveFilaments = await this.getResolvedPrinterFilamentInventory(host, bambuSerial, bambuToken, printModel, printNozzle);
2607
- if (liveFilaments.recommended?.load_filaments) {
2608
- autoSliceOptions.loadFilaments = liveFilaments.recommended.load_filaments;
2609
- }
2610
- }
2611
- catch (filamentError) {
2612
- console.warn("Could not resolve live printer filaments for auto-slicing:", filamentError);
2641
+ // Inspect and auto-slice before upload. Preserve any failure instead of sending an unsliced project.
2642
+ const JSZip = (await import('jszip')).default;
2643
+ const zipData = fs.readFileSync(threeMFPath);
2644
+ const zip = await JSZip.loadAsync(zipData);
2645
+ const hasGcode = Object.values(zip.files).some(entry => !entry.dir && /\.gcode$/i.test(entry.name));
2646
+ if (!hasGcode) {
2647
+ if (printBedType === "supertack_plate") {
2648
+ throw new Error('BambuStudio CLI SuperTack bed type is not verified; use a pre-sliced 3MF for SuperTack or choose textured_plate, cool_plate, engineering_plate, or hot_plate.');
2649
+ }
2650
+ console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
2651
+ const autoSliceOptions = {
2652
+ uptodate: true,
2653
+ ensureOnBed: true,
2654
+ minSave: true,
2655
+ skipModifiedGcodes: true,
2656
+ bedType: printBedType,
2657
+ };
2658
+ if (!explicitSlicerProfile) {
2659
+ try {
2660
+ const liveFilaments = await this.getResolvedPrinterFilamentInventory(host, bambuSerial, bambuToken, printModel, printNozzle);
2661
+ if (liveFilaments.recommended?.load_filaments) {
2662
+ autoSliceOptions.loadFilaments = liveFilaments.recommended.load_filaments;
2613
2663
  }
2614
2664
  }
2615
- threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, slicerPath, activeSlicerProfile, undefined, // progressCallback
2616
- printPreset, autoSliceOptions);
2617
- console.log("Auto-sliced to: " + threeMFPath);
2618
- }
2619
- }
2620
- catch (sliceCheckErr) {
2621
- if (String(sliceCheckErr?.message || "").includes("SuperTack")) {
2622
- throw sliceCheckErr;
2665
+ catch (filamentError) {
2666
+ console.warn("Could not resolve live printer filaments for auto-slicing:", filamentError);
2667
+ }
2623
2668
  }
2624
- console.warn("Could not check/slice 3MF, proceeding with original:", sliceCheckErr.message);
2669
+ threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, slicerPath, activeSlicerProfile, undefined, // progressCallback
2670
+ printPreset, autoSliceOptions);
2671
+ console.log("Auto-sliced to: " + threeMFPath);
2625
2672
  }
2626
2673
  const parsed3MFData = await parse3MF(threeMFPath);
2627
2674
  const isH2Print = H2_BAMBU_MODELS.has(printModel);
@@ -2783,16 +2830,13 @@ class BambuPrinterMCPServer {
2783
2830
  throw new Error("Missing required parameter: stl_path");
2784
2831
  result = await this.stlManipulator.layFlat(String(args.stl_path));
2785
2832
  break;
2833
+ case "blender_mcp_status":
2834
+ result = await this.blender.status(args ?? {}, extra.signal);
2835
+ break;
2836
+ case "blender_mcp_call":
2837
+ return await this.blender.call(args ?? {}, extra.signal);
2786
2838
  case "blender_mcp_edit_model":
2787
- if (!args?.stl_path || !Array.isArray(args.operations)) {
2788
- throw new Error("Missing required parameters: stl_path and operations");
2789
- }
2790
- result = await this.invokeBlenderBridge({
2791
- stlPath: String(args.stl_path),
2792
- operations: args.operations.map((entry) => String(entry)),
2793
- execute: Boolean(args.execute ?? false),
2794
- bridgeCommand: this.resolveExecutableSelectorArg(args.bridge_command, "blender_mcp_edit_model", "bridge_command", true) ?? this.runtimeConfig.blenderBridgeCommand,
2795
- });
2839
+ result = await this.blender.edit(args ?? {}, this.resolveExecutableSelectorArg(args?.bridge_command, "blender_mcp_edit_model", "bridge_command", true) ?? (!process.env.BLENDER_MCP_COMMAND?.trim() ? this.runtimeConfig.blenderBridgeCommand : undefined), extra.signal);
2796
2840
  break;
2797
2841
  default:
2798
2842
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
@@ -2823,38 +2867,28 @@ class BambuPrinterMCPServer {
2823
2867
  }
2824
2868
  });
2825
2869
  }
2826
- async invokeBlenderBridge(params) {
2827
- const payload = {
2828
- stlPath: params.stlPath,
2829
- operations: params.operations,
2830
- };
2831
- if (!params.execute || !params.bridgeCommand) {
2832
- return {
2833
- status: "prepared",
2834
- payload,
2835
- note: params.bridgeCommand
2836
- ? "Set execute=true to run the Blender bridge command."
2837
- : "No BLENDER_MCP_BRIDGE_COMMAND configured. Set the env var or pass bridge_command.",
2838
- };
2839
- }
2840
- const { execFile } = await import("node:child_process");
2841
- const { promisify } = await import("node:util");
2842
- const execFileAsync = promisify(execFile);
2843
- const { stdout, stderr } = await execFileAsync(params.bridgeCommand, [], {
2844
- env: { ...process.env, MCP_BLENDER_PAYLOAD: JSON.stringify(payload) },
2845
- timeout: 120000,
2846
- });
2847
- return {
2848
- status: "executed",
2849
- stdout: stdout.trim(),
2850
- stderr: stderr.trim(),
2851
- };
2852
- }
2853
2870
  async startStdio() {
2854
2871
  const transport = new StdioServerTransport();
2855
2872
  await this.server.connect(transport);
2873
+ process.stdin.once("end", () => { void this.shutdown(); });
2856
2874
  console.error("Bambu Printer MCP server running on stdio");
2857
2875
  }
2876
+ async shutdown(exitCode = 0) {
2877
+ if (this.shuttingDown)
2878
+ return;
2879
+ this.shuttingDown = true;
2880
+ try {
2881
+ // Closing the MCP transport aborts its active requests before exit cleanup.
2882
+ await this.server.close();
2883
+ this.httpRuntime?.httpServer.close();
2884
+ }
2885
+ catch (error) {
2886
+ console.error("[MCP Shutdown]", error);
2887
+ }
2888
+ finally {
2889
+ process.exit(exitCode);
2890
+ }
2891
+ }
2858
2892
  async startHttp() {
2859
2893
  const { httpHost, httpPort, httpPath, statefulSession, enableJsonResponse, allowedOrigins } = this.runtimeConfig;
2860
2894
  const transport = new StreamableHTTPServerTransport({
@@ -2894,4 +2928,6 @@ class BambuPrinterMCPServer {
2894
2928
  }
2895
2929
  }
2896
2930
  const server = new BambuPrinterMCPServer();
2931
+ process.once("SIGINT", () => { void server.shutdown(130); });
2932
+ process.once("SIGTERM", () => { void server.shutdown(143); });
2897
2933
  server.run().catch(console.error);
@@ -49,6 +49,9 @@ const H2_MODEL_NAMES = new Set(["h2", "h2c", "h2d", "h2dpro", "h2d pro", "h2s"])
49
49
  function isH2ModelName(model) {
50
50
  return H2_MODEL_NAMES.has(String(model ?? "").trim().toLowerCase().replace(/\s+/g, " "));
51
51
  }
52
+ function isP2SModelName(model) {
53
+ return String(model ?? "").trim().toLowerCase() === "p2s";
54
+ }
52
55
  function sleep(ms) {
53
56
  return new Promise((resolve) => setTimeout(resolve, ms));
54
57
  }
@@ -88,6 +91,10 @@ class TolerantBambuClient extends BambuClient {
88
91
  return "H2S";
89
92
  if (sn.startsWith("094"))
90
93
  return "H2D";
94
+ if (sn.startsWith("239"))
95
+ return "H2C";
96
+ if (sn.startsWith("31B"))
97
+ return "H2DPRO";
91
98
  if (sn.startsWith("00M"))
92
99
  return "X1C";
93
100
  if (sn.startsWith("00W"))
@@ -98,6 +105,8 @@ class TolerantBambuClient extends BambuClient {
98
105
  return "P1P";
99
106
  if (sn.startsWith("01P"))
100
107
  return "P1S";
108
+ if (sn.startsWith("22E"))
109
+ return "P2S";
101
110
  if (sn.startsWith("030"))
102
111
  return "A1";
103
112
  if (sn.startsWith("039"))
@@ -457,24 +466,33 @@ export class BambuImplementation {
457
466
  let remoteFileName = path.basename(options.filePath);
458
467
  remoteFileName = remoteFileName.replace(/\.gcode\.3mf\.gcode\.3mf$/i, ".gcode.3mf");
459
468
  // H2-series printers land files at the FTP root and reference them via ftp:///<name>.
460
- // P1/A1/X1 use /cache/<name> and file:///sdcard/cache/<name>.
469
+ // Full-size A1 uses SD root/project_file (reported on firmware 01.08.01.00).
470
+ // P1/X1/A1 mini retain their legacy cache/gcode_file route.
471
+ // P2S keeps the /cache/<name> upload but only accepts the H2-style
472
+ // project_file command, referenced via ftp:///cache/<name> (verified on
473
+ // P2S firmware 01.02.00.00; file:///sdcard/... fails with ERROR STATE).
461
474
  const isH2 = serial.startsWith("093") ||
462
475
  serial.startsWith("094") ||
463
476
  isH2ModelName(options.bambuModel);
464
- const remoteProjectPath = isH2 ? remoteFileName : `cache/${remoteFileName}`;
465
- const remoteUploadPath = isH2 ? `/${remoteFileName}` : `/cache/${remoteFileName}`;
466
- const projectUrl = isH2
467
- ? `ftp:///${remoteFileName}`
477
+ const isP2S = serial.startsWith("22E") || isP2SModelName(options.bambuModel);
478
+ const isA1 = String(options.bambuModel ?? "").trim().toLowerCase() === "a1" ||
479
+ (!options.bambuModel && serial.startsWith("030"));
480
+ const usesH2ProjectFile = isH2 || isP2S;
481
+ const remoteProjectPath = isH2 || isA1 ? remoteFileName : `cache/${remoteFileName}`;
482
+ const remoteUploadPath = `/${remoteProjectPath}`;
483
+ const projectUrl = usesH2ProjectFile
484
+ ? `ftp:///${remoteProjectPath}`
468
485
  : `file:///sdcard/${remoteProjectPath}`;
469
486
  // Upload via basic-ftp directly (bypasses bambu-js double-path bug)
470
487
  await this.ftpUpload(host, token, options.filePath, remoteUploadPath);
471
488
  // Pre-sliced .gcode.3mf files: routing depends on firmware generation.
472
- // P1/A1/X1 series: project_file returns 405004002 for .gcode.3mf (firmware
489
+ // Legacy P1/X1/A1 mini: project_file returns 405004002 for .gcode.3mf (firmware
473
490
  // doesn't recognise the container), so use gcode_file instead.
474
- // H2-series: gcode_file is not supported; project_file works because the
491
+ // H2-series and P2S: gcode_file is not supported (P2S answers 0500-4002
492
+ // "Unsupported file path or name"); project_file works because the
475
493
  // firmware can open the zip and find Metadata/plate_<n>.gcode directly.
476
494
  if (options.filePath.toLowerCase().endsWith(".gcode.3mf")) {
477
- if (!isH2) {
495
+ if (!usesH2ProjectFile && !isA1) {
478
496
  const printer = await this.getPrinter(host, serial, token);
479
497
  await invokeWithoutAck(printer, new GCodeFileCommand({ fileName: remoteProjectPath }));
480
498
  return {
@@ -483,7 +501,7 @@ export class BambuImplementation {
483
501
  remoteProjectPath,
484
502
  };
485
503
  }
486
- // H2-series: fall through to project_file path below
504
+ // H2-series and P2S: fall through to project_file path below
487
505
  }
488
506
  const projectMetadata = await this.resolveProjectFileMetadata(options.filePath, options.plateIndex);
489
507
  // Send project_file command via bambu-node MQTT (bypasses bambu-js
@@ -550,7 +568,7 @@ export class BambuImplementation {
550
568
  }
551
569
  let amsMapping;
552
570
  let amsMapping2;
553
- if (isH2) {
571
+ if (usesH2ProjectFile) {
554
572
  const projLen = Math.max(projectMetadata.projectFilamentCount, baseMapping.length, 1);
555
573
  amsMapping = Array.from({ length: projLen }, (_, i) => i < baseMapping.length ? baseMapping[i] : -1);
556
574
  amsMapping2 = amsMapping.map((v) => {
@@ -564,12 +582,12 @@ export class BambuImplementation {
564
582
  });
565
583
  }
566
584
  else {
567
- amsMapping = Array.from({ length: 5 }, (_, i) => i < baseMapping.length ? baseMapping[i] : -1);
585
+ amsMapping = Array.from({ length: Math.max(5, baseMapping.length, projectMetadata.projectFilamentCount) }, (_, i) => i < baseMapping.length ? baseMapping[i] : -1);
568
586
  amsMapping2 = [];
569
587
  }
570
588
  const b = (v) => (v ? 1 : 0);
571
589
  let projectFileCmd;
572
- if (isH2) {
590
+ if (usesH2ProjectFile) {
573
591
  const submissionId = String(Date.now() & 0x7fffffff);
574
592
  projectFileCmd = {
575
593
  print: {
@@ -994,7 +1012,7 @@ export class BambuImplementation {
994
1012
  // integration's models.py shows the printer reports its own
995
1013
  // `ipcam.rtsp_url` for these models, and Parker (H2S) rejects the
996
1014
  // A1/P1 80-byte auth packet on port 6000 (verified 2026-04-27 --
997
- // see PROGRESS.md "H2 probe results").
1015
+ // confirmed by local H2 camera transport probes).
998
1016
  const RTSP_MODELS = new Set([
999
1017
  "x1", "x1c", "x1carbon", "x1e", "p2s",
1000
1018
  "h2", "h2s", "h2d", "h2c", "h2dpro",
@@ -10,11 +10,13 @@
10
10
  *
11
11
  * This module:
12
12
  * 1. Indexes every BBL profile JSON by its `name` field.
13
- * 2. Recursively walks `inherits`, deep-merging parent into child.
13
+ * 2. Recursively walks `inherits`, deep-merging parent into child, and
14
+ * applies each level's `include` templates (G-code templates for
15
+ * machines, per-variant defaults for filaments) the way the GUI does.
14
16
  * 3. Derives `nozzle_volume_type` from `default_nozzle_volume_type[0]`
15
17
  * (the GUI does this implicitly; the CLI doesn't).
16
- * 4. Merges CLI-specific machine_limits from `BBL/cli_config.json` so the
17
- * printer doesn't run unsafe accelerations / jerks.
18
+ * 4. Validates the model in `BBL/cli_config.json` and merges its CLI-specific
19
+ * machine_limits where supplied for safe accelerations / jerks.
18
20
  * 5. Writes the flattened JSON to a temp file the caller passes to
19
21
  * BambuStudio CLI.
20
22
  *
@@ -32,6 +34,9 @@ export interface FlattenedProfiles {
32
34
  machineLeafName: string;
33
35
  processLeafName: string;
34
36
  filamentLeafNames: string[];
37
+ /** False only for an explicit standalone custom machine. */
38
+ cliConfigValidated: boolean;
39
+ /** Some validated official model configs intentionally have no limits. */
35
40
  cliOverlayApplied: boolean;
36
41
  };
37
42
  }
@@ -59,6 +64,12 @@ export interface FlattenOptions {
59
64
  nozzleVolumeType?: "Standard" | "High Flow";
60
65
  /** BambuStudio display name, e.g. "Textured PEI Plate" or "Cool Plate". */
61
66
  bedType?: string;
67
+ /** Actual input configs, including user overrides on top of BBL parents. */
68
+ sourceProfiles?: {
69
+ machine?: Record<string, unknown>;
70
+ process?: Record<string, unknown>;
71
+ filaments?: (Record<string, unknown> | undefined)[];
72
+ };
62
73
  }
63
74
  /**
64
75
  * Flatten the leaf profiles, post-process for CLI, and write to temp files.
@@ -68,7 +79,7 @@ export interface FlattenOptions {
68
79
  export declare function flattenForCli(opts: FlattenOptions): Promise<FlattenedProfiles>;
69
80
  /**
70
81
  * Given the SLICER_PATH (path to BambuStudio executable), walk up to the
71
- * Resources/profiles directory. Falls back to common platform paths.
82
+ * profile directory for that installation (macOS, Windows, or Linux prefix).
72
83
  *
73
84
  * Override via BAMBU_PROFILES_ROOT env.
74
85
  */