mcp-3d-printer-server 1.2.12 → 1.2.14

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/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.14 — 2026-10-25
4
+
5
+ Ported from [bambu-printer-mcp](https://github.com/DMontgomery40/bambu-printer-mcp) 1.1.34. Its evidence came from real P1S runs and an x86-64 Linux FULU container; this repository's validation is noted in each entry.
6
+
7
+ ### Fixed
8
+
9
+ - Let H2D printers with stock nozzles print. Every print stopped with `Printer nozzle 0 information is unreliable, abnormal or unknown` because the HS01 nozzles report `stat: 1` permanently. Bambu Studio reads bit 0 of `stat` as "information unreliable" and bits 1-2 as abnormal or unknown. Nozzles that report no identity chip (`sn: "N/A"`) now pass bit 0, and their configured diameter and type are still compared with the job. The Bambu print prompt names them so a person confirms they match the installed hotends, and checks log them once per printer on stderr, so a headless setup still records them. Abnormal or unknown nozzles still block, and a chipped nozzle with unreliable information asks for a refresh on the printer. Reported by [MorganFreeeman](https://github.com/MorganFreeeman) in [bambu-printer-mcp #67](https://github.com/DMontgomery40/bambu-printer-mcp/issues/67) with idle and mid-print H2D firmware 01.04.00.00 reports. Validated here with that report in mocked-status regressions; no H2D hardware was available.
10
+ - Say "declined or cancelled" when a person or the MCP client declines a hardware confirmation, instead of advising a client with elicitation support; clients without elicitation still get that advice. Ordinary print and heating prompts name the opt-out that applies to the printer: `PRINT_REQUIRE_CONFIRMATION=0`, or also `BAMBU_REQUIRE_CONFIRMATION=0` for Bambu printers. Bed-clearance confirmations say a person must accept them. Also from [bambu-printer-mcp #67](https://github.com/DMontgomery40/bambu-printer-mcp/issues/67); validated with mocked Bambu and generic adapters.
11
+ - Stop `get_printer_status` on Bambu printers from reporting `UNKNOWN` when the printer's first MQTT push is a sparse hello (model and modules). The read returned as soon as any data had arrived; it now waits, within its existing 1.5 s bound, until the full status has merged. bambu-printer-mcp saw this twice on a P1S; validated here with a delayed-push regression.
12
+ - Stop `slice_stl` option combinations from crashing OrcaSlicer's CLI. OrcaSlicer 2.4.2 segfaults in `PartPlate::check_outside`, which needs the GUI, when `scale` or `rotate_x`/`rotate_y` moves an object below the bed while arranging, the default for STL input. For STL input with OrcaSlicer or FULU, the server applies scale and rotations to a copy of the mesh in Orca's own order (scale, then X, then Y, then Z) and omits those flags; for 3MF input with `arrange: true` it refuses that combination with an explanation. `repetitions` on an STL now uses Orca's `--clone-objects`, which cannot be combined with `clone_objects`. The `repetitions`, `arrange` and `clone_objects` descriptions now state the CLI's real limits. Bambu Studio keeps every flag. bambu-printer-mcp verified the crash and fix against OrcaSlicer 2.4.2 on Linux, including the 10 × 20 × 30 mm rotation-order check; this repository's regressions use a fake CLI that records the mesh it receives.
13
+ - Report how a Bambu-compatible slicer failed: Bambu Studio and OrcaSlicer failures now name the CLI's own reason, from the `result.json` it writes on Linux or from its exit code, next to the existing exit code or signal and output tails. Validated with a fake CLI.
14
+ - Make FULU BambuNetwork bridge probes work with FULU's v1.0.0 Linux host. The host exits when its input closes, dropping requests it has not answered, so every probe and RPC call returned no responses; the server now keeps the input open until each request is answered. On Linux it passes `PJARCZAK_BAMBU_NETWORK_SO` and `PJARCZAK_BAMBU_SOURCE_SO` inside `PJARCZAK_BAMBU_PLUGIN_DIR` unless set, because the host opens them from its working directory ([FULU #51](https://github.com/FULU-Foundation/OrcaSlicer-bambulab/issues/51)). The default Linux bridge command pipes the host through `cat`, because the host exits 101 on the socket stdout Node.js provides ([FULU #50](https://github.com/FULU-Foundation/OrcaSlicer-bambulab/issues/50)); probes and RPC results explain exit code 101, and the FULU guide documents a wrapper for custom commands. A handshake from a host that could not load the networking plug-in is no longer reported as ready, and `file open failed` names the missing setting. A host that stops answering times out with its stderr, how many requests went unanswered and any hints, and Linux setup checks report the library paths that reach the host, including ones you set. macOS bundle notes and wrapper commands appear only for macOS, and the wrapper command only when the wrapper exists. Linux host bugs were reported with diagnosis and upstream issues by [Aaron Roberts (aaroberts)](https://github.com/aaroberts) in [bambu-printer-mcp #68](https://github.com/DMontgomery40/bambu-printer-mcp/issues/68). Validated with a fake host that exits on end of input, and in the maintainer's x86-64 Linux container (QEMU emulation) against FULU's real v1.0.0 host and networking plug-in 02.05.02.58: probes were ready through the default command and the wrapper, a directly launched host returned exit 101 with its explanation, and a missing plug-in folder returned the `file open failed` explanation. `net.create_agent` through `fulu_bambu_network_rpc` created an agent; the host then aborted while exiting, after answering. No printer was contacted. bambu-printer-mcp's bridge LAN printing, certificate, `-4030` and job-name changes do not apply: this server refuses raw bridge print and printer-connection methods and prints over its direct MQTT/FTPS path.
15
+
16
+ ### Documentation
17
+
18
+ - Add the FULU guide's Linux host setup and an "Upstream bugs and workarounds" table naming FULU #50 and #51, the FULU runtime's missing `printer.cer`, the FULU v1.0.0 CLI crash, the OrcaSlicer 2.4.2 `check_outside` crash (related to the closed [OrcaSlicer #13328](https://github.com/OrcaSlicer/OrcaSlicer/issues/13328)), and the Bambu plug-in's `-4030` design, with what this server does about each.
19
+
20
+ ### Dependencies
21
+
22
+ - Move the direct `zod` dependency to 4.6.7 for [GHSA-mrhx-c7xc-m3w5](https://github.com/advisories/GHSA-mrhx-c7xc-m3w5), memory exhaustion when validating oversized constrained collections; every zod 3 release is affected. The MCP SDK and zod-to-json-schema both accept zod 4, and this server does not import zod itself. Update body-parser to 2.4.0 ([GHSA-27r3-jxmh-p828](https://github.com/advisories/GHSA-27r3-jxmh-p828)), fast-uri to 3.1.9, follow-redirects to 1.16.2, hono to 4.13.14 and ip-address to 10.7.5 for their new advisories. `npm audit` now reports no vulnerabilities. The documentation site's VitePress development server still pulls vite 6.4.3 and esbuild with advisories that have no VitePress 1.x fix; they affect only `vitepress dev`, not the published static site.
23
+
24
+ ## 1.2.13 — 2026-10-20
25
+
26
+ ### Dependencies
27
+
28
+ - Refresh the reproducible installation/test lock from MQTT.js 5.10.4 to 5.16.1, including its fix for reconnecting when the previous stream is already destroyed. Keep bambu-node pinned at 3.22.21 and preserve the server's checked MQTT wire format and rejection guard. Thank you to [Henk Kraal (hkraal)](https://github.com/hkraal) and the [MQTT.js maintainers](https://github.com/mqttjs/MQTT.js/releases/tag/v5.16.1).
29
+ - Update the documentation build's source-map-js to 1.2.2 for [GHSA-68fv-2mgg-jv7q](https://github.com/advisories/GHSA-68fv-2mgg-jv7q), which addresses indexed source-map offsets blocking the event loop. Thank you to [7rulnik](https://github.com/7rulnik/source-map-js/pull/79). Refresh compatible Node 18 typings and MQTT's required transitive packages.
30
+
31
+ The root lock controls `npm ci`, including CI and reproducible local installs; npm does not ship that lock to consumers. Existing parent projects can retain older transitive MQTT versions and must refresh their own locks. This is not a new published MQTT minimum or a claim that every advisory is resolved. Remaining nodemon/chokidar/braces and VitePress/Vite/esbuild development-server advisories have no compatible automatic fix; no major upgrade or downgrade is forced. Printer safety behavior is unchanged and no physical printer operation is performed.
32
+
3
33
  ## 1.2.12 — 2026-10-13
4
34
 
5
35
  ### Fixed
package/CONTRIBUTORS.md CHANGED
@@ -21,6 +21,8 @@ Thank you to everyone who builds, tests, reports problems, and shares real print
21
21
 
22
22
  | Contributor | Contribution |
23
23
  | --- | --- |
24
+ | [MorganFreeeman](https://github.com/MorganFreeeman) | Reports in the Bambu fork's [#67](https://github.com/DMontgomery40/bambu-printer-mcp/issues/67) that every H2D print stopped on stock nozzles, with idle and mid-print H2D firmware 01.04.00.00 reports, and that a declined confirmation pointed to the wrong cause. The nozzle-information and confirmation-wording fixes are ported here with that report as a mocked-status regression; no H2D print is claimed. |
25
+ | [Aaron Roberts (aaroberts)](https://github.com/aaroberts) | Diagnoses FULU's Linux host failures under Node.js in the Bambu fork's [#68](https://github.com/DMontgomery40/bambu-printer-mcp/issues/68), with the upstream issues ([FULU #50](https://github.com/FULU-Foundation/OrcaSlicer-bambulab/issues/50), [FULU #51](https://github.com/FULU-Foundation/OrcaSlicer-bambulab/issues/51)) and a working configuration. The Linux library paths, exit-101 explanation and `cat` wrapper are ported here and checked against FULU's real Linux host without contacting a printer. |
24
26
  | [Steavie](https://github.com/Steavie) | Reports reversed A1/A1 mini serial identity in the Bambu fork's [#39](https://github.com/DMontgomery40/bambu-printer-mcp/issues/39). This repository now uses the corrected prefixes for fresh safety identity, with mocked MQTT regressions. |
25
27
  | [Travis Cashman (travismcashan)](https://github.com/travismcashan) | Reports MCP stdout pollution from model parsing and slicing in the Bambu fork's [#44](https://github.com/DMontgomery40/bambu-printer-mcp/issues/44). The stderr correction and subprocess regressions are also applied here. Their exact stock material-switch and runout-purge evidence informs the X2D file-inspection port; maintainer validation remains offline and mocked. |
26
28
  | [Lickitysplitted](https://github.com/Lickitysplitted) | Diagnoses Bambu FTPS "Premature close" upload failures on an X1C as missing TLS session reuse in [#22](https://github.com/DMontgomery40/mcp-3D-printer-server/issues/22). Release 1.2.9 addresses it; confirmation on that X1C firmware is still outstanding. |
@@ -51,6 +53,8 @@ See the [full contribution history](https://github.com/DMontgomery40/mcp-3D-prin
51
53
 
52
54
  Thanks to [Patrick Juchli](https://github.com/patrickjuchli) and the basic-ftp maintainers for [6.2.3](https://github.com/patrickjuchli/basic-ftp/releases/tag/v6.2.3), which fixes rejected-command crashes, client recovery, and password exposure in PASS errors. The compatible FTPS security update is validated in this release.
53
55
 
56
+ Thanks to [Henk Kraal (hkraal)](https://github.com/hkraal) and the [MQTT.js maintainers](https://github.com/mqttjs/MQTT.js/releases/tag/v5.16.1) for correcting reconnect behavior when the previous stream is destroyed, and to [7rulnik](https://github.com/7rulnik/source-map-js/pull/79) for the indexed source-map offset security fix in source-map-js 1.2.2. Their fixes are incorporated into the installation/test and documentation locks; published consumers still manage their own transitive locks. Thanks also to the [DefinitelyTyped contributors](https://github.com/DefinitelyTyped/DefinitelyTyped) for maintained Node 18 and stream typings.
57
+
54
58
  ## Open-source community and interoperability
55
59
 
56
60
  Special thanks to the [FULU Foundation](https://www.fulu.org/), [Louis Rossmann](https://www.youtube.com/@rossmanngroup), and the [OrcaSlicer-bambulab contributors](https://github.com/FULU-Foundation/OrcaSlicer-bambulab) for their work supporting user control and interoperable tools. This is community recognition, separate from code authorship in this repository. The Bambu-only fork, [bambu-printer-mcp](https://github.com/DMontgomery40/bambu-printer-mcp), credits its own contributors in its [CONTRIBUTORS.md](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/CONTRIBUTORS.md).
package/README.md CHANGED
@@ -590,8 +590,8 @@ Options for the Bambu-compatible path:
590
590
  | `filament_colours` | One `#RRGGBB` per filament slot, `;`-separated. Defaults to the input 3MF's colours, then each profile's colour |
591
591
  | `template_3mf_path`, `template_name`, `template_dir` | Reuse a 3MF's or profile's slicer settings as the process profile (defaults: `BAMBU_TEMPLATE_3MF_PATH`, `BAMBU_TEMPLATE_DIR`). An explicit `slicer_profile` takes precedence |
592
592
  | `uptodate`, `min_save`, `skip_modified_gcodes` | Refresh 3MF presets to the installed slicer, write a smaller 3MF, or ignore custom G-code embedded in an input 3MF |
593
- | `orient`, `arrange`, `ensure_on_bed`, `repetitions`, `clone_objects`, `skip_objects`, `slice_plate` | Placement: auto-orient, auto-arrange (set `false` to keep a layout), drop floating models onto the bed, copies, per-object clone counts, objects to skip, and which plate to slice (0 = all) |
594
- | `scale`, `rotate`, `rotate_x`, `rotate_y` | Transform before slicing (uniform scale; rotations in degrees) |
593
+ | `orient`, `arrange`, `ensure_on_bed`, `repetitions`, `clone_objects`, `skip_objects`, `slice_plate` | Placement: auto-orient, auto-arrange (STL input is arranged by default; set `false` to keep a 3MF layout), drop floating models onto the bed, copies (OrcaSlicer copies an STL with `--clone-objects`), a total clone count for the loaded model, objects to skip, and which plate to slice (0 = all) |
594
+ | `scale`, `rotate`, `rotate_x`, `rotate_y` | Transform before slicing (uniform scale; rotations in degrees). For OrcaSlicer STL input the server applies them to a mesh copy in Orca's order to avoid a CLI crash; see [OrcaSlicer CLI limits](https://github.com/DMontgomery40/mcp-3D-printer-server/blob/main/docs/SLICING.md#bambu-compatible-slicing) |
595
595
  | `enable_timelapse`, `allow_mix_temp` | Insert timelapse parking moves; allow filaments with different temperature needs on one plate |
596
596
 
597
597
  Generic slicers keep their own profile formats: PrusaSlicer and Slic3r load one exported config, and generic OrcaSlicer takes `machine.json;process.json`, optionally followed by `|filament.json`. `slicer_type`, `slicer_profile`, and `filament_profile` fall back to `SLICER_TYPE`, `SLICER_PROFILE`, and `FILAMENT_PROFILE`. The slicer executable comes from `SLICER_PATH`; a per-call `slicer_path` requires `MCP_ALLOW_EXECUTABLE_ARG=1`. See the [slicing guide](https://github.com/DMontgomery40/mcp-3D-printer-server/blob/main/docs/SLICING.md#bambu-compatible-slicing).
@@ -16,5 +16,13 @@ export type FuluBridgeRpcOptions = {
16
16
  bambuModel?: string;
17
17
  };
18
18
  export declare function isFuluPrintRpcMethod(method: string): boolean;
19
+ /**
20
+ * FULU's Linux host opens its network and source libraries by bare name from
21
+ * its working directory instead of PJARCZAK_BAMBU_PLUGIN_DIR
22
+ * (FULU-Foundation/OrcaSlicer-bambulab#51), so name them in that directory
23
+ * unless they are already set. macOS wrappers run the host in a Linux guest
24
+ * that may not see host paths, so they are left alone.
25
+ */
26
+ export declare function linuxPluginLibraryEnv(env: NodeJS.ProcessEnv, platform: NodeJS.Platform): Record<string, string>;
19
27
  export declare function invokeFuluBridgeRpc(options: FuluBridgeRpcOptions): Promise<Record<string, unknown>>;
20
28
  export declare function inspectFuluOrcaSetup(options: FuluOrcaSetupOptions): Promise<Record<string, unknown>>;
package/dist/fulu-orca.js CHANGED
@@ -47,6 +47,43 @@ const SESSION_MUTATING_METHODS = new Set([
47
47
  export function isFuluPrintRpcMethod(method) {
48
48
  return FULU_PRINT_RPC_METHODS.has(method.trim());
49
49
  }
50
+ const FULU_LINUX_DOCS = "https://github.com/DMontgomery40/mcp-3D-printer-server/blob/main/docs/FULU.md#linux";
51
+ /**
52
+ * FULU's Linux host opens its network and source libraries by bare name from
53
+ * its working directory instead of PJARCZAK_BAMBU_PLUGIN_DIR
54
+ * (FULU-Foundation/OrcaSlicer-bambulab#51), so name them in that directory
55
+ * unless they are already set. macOS wrappers run the host in a Linux guest
56
+ * that may not see host paths, so they are left alone.
57
+ */
58
+ export function linuxPluginLibraryEnv(env, platform) {
59
+ const pluginDir = env.PJARCZAK_BAMBU_PLUGIN_DIR?.trim();
60
+ if (platform !== "linux" || !pluginDir)
61
+ return {};
62
+ const libraries = {};
63
+ if (!env.PJARCZAK_BAMBU_NETWORK_SO?.trim())
64
+ libraries.PJARCZAK_BAMBU_NETWORK_SO = path.resolve(pluginDir, "libbambu_networking.so");
65
+ if (!env.PJARCZAK_BAMBU_SOURCE_SO?.trim())
66
+ libraries.PJARCZAK_BAMBU_SOURCE_SO = path.resolve(pluginDir, "libBambuSource.so");
67
+ return libraries;
68
+ }
69
+ /** FULU's Linux host exits 101 when it cannot reopen a socket stdout (FULU-Foundation/OrcaSlicer-bambulab#50). */
70
+ const STDOUT_SOCKET_HINT = "Exit code 101 means FULU's Linux host could not reopen its stdout, which fails when stdout is a socket, as Node.js provides " +
71
+ "(https://github.com/FULU-Foundation/OrcaSlicer-bambulab/issues/50). Launch the host through a wrapper that pipes its output through cat: " +
72
+ FULU_LINUX_DOCS;
73
+ /** Actionable hints for a bridge run: the exit-101 socket bug and a host that cannot open the plug-in libraries. */
74
+ function bridgeHints(exitCode, responses) {
75
+ const hints = [];
76
+ if (exitCode === 101)
77
+ hints.push(STDOUT_SOCKET_HINT);
78
+ const fileOpenFailure = Object.values(responses)
79
+ .find((value) => [value?.network_status, value?.error].some((text) => typeof text === "string" && /file open failed/i.test(text)));
80
+ if (fileOpenFailure) {
81
+ hints.push(`FULU BambuNetwork bridge could not open its networking library (${String(fileOpenFailure.network_status ?? fileOpenFailure.error)}). ` +
82
+ "Set PJARCZAK_BAMBU_PLUGIN_DIR to the directory containing libbambu_networking.so and libBambuSource.so, " +
83
+ `or set PJARCZAK_BAMBU_NETWORK_SO and PJARCZAK_BAMBU_SOURCE_SO to their absolute paths: ${FULU_LINUX_DOCS}`);
84
+ }
85
+ return hints;
86
+ }
50
87
  function normalizePlatform(value) {
51
88
  const normalized = (value || process.platform).trim().toLowerCase();
52
89
  if (normalized === "mac" || normalized === "macos" || normalized === "darwin") {
@@ -200,7 +237,7 @@ function setupCommands(platform, pluginDir, runtimeDir) {
200
237
  const verify = path.join(pluginDir, "verify_runtime_macos.sh");
201
238
  const bridgeHost = path.join(runtimeDir || "", "pjarczak_bambu_linux_host");
202
239
  const wrapper = path.join(pluginDir, "pjarczak-bambu-linux-host-wrapper");
203
- return [
240
+ const commands = [
204
241
  {
205
242
  label: "install macOS Lima runtime",
206
243
  command: `bash ${quoteShell(install)} -PackageDir ${quoteShell(pluginDir)} -PluginDir ${quoteShell(pluginDir)}`,
@@ -213,11 +250,15 @@ function setupCommands(platform, pluginDir, runtimeDir) {
213
250
  label: "verify macOS Lima runtime core before Linux plugin payload is present",
214
251
  command: `bash ${quoteShell(verify)} -PackageDir ${quoteShell(pluginDir)} -PluginDir ${quoteShell(pluginDir)} -AllowMissingLinuxPlugin`,
215
252
  },
216
- {
253
+ ];
254
+ // The wrapper ships in FULU's macOS bundle; suggest it only where it exists.
255
+ if (fs.existsSync(wrapper)) {
256
+ commands.push({
217
257
  label: "bridge command for FULU BambuNetwork runtime",
218
258
  command: `${quoteShell(wrapper)} ${quoteShell(bridgeHost)}`,
219
- },
220
- ];
259
+ });
260
+ }
261
+ return commands;
221
262
  }
222
263
  if (platform === "win32") {
223
264
  return [
@@ -244,6 +285,11 @@ function setupCommands(platform, pluginDir, runtimeDir) {
244
285
  label: "configure MCP slicer path",
245
286
  command: "SLICER_TYPE=orcaslicer-bambulab SLICER_PATH=/path/to/orcaslicer",
246
287
  },
288
+ {
289
+ label: "bridge command for FULU's Linux host, piped through cat (FULU #50)",
290
+ command: `FULU_BAMBU_BRIDGE_COMMAND="${quoteShell(path.join(pluginDir, "pjarczak_bambu_linux_host"))} | cat" ` +
291
+ "PJARCZAK_BAMBU_PLUGIN_DIR=\"$HOME/.config/OrcaSlicer/plugins\"",
292
+ },
247
293
  ];
248
294
  }
249
295
  function defaultBridgeCommand(platform, pluginDir, runtimeDir) {
@@ -255,12 +301,16 @@ function defaultBridgeCommand(platform, pluginDir, runtimeDir) {
255
301
  }
256
302
  if (platform === "darwin") {
257
303
  const wrapper = path.join(pluginDir, "pjarczak-bambu-linux-host-wrapper");
304
+ if (!fs.existsSync(wrapper))
305
+ return undefined;
258
306
  const runtimeHost = path.join(runtimeDir || defaultRuntimeDir(platform) || "", "pjarczak_bambu_linux_host");
259
307
  return `${quoteShell(wrapper)} ${quoteShell(runtimeHost)}`;
260
308
  }
261
309
  if (platform === "linux") {
310
+ // The v1.0.0 host exits 101 on the socket stdout Node.js gives a child
311
+ // (FULU-Foundation/OrcaSlicer-bambulab#50); a shell pipe through cat avoids it.
262
312
  const host = path.join(pluginDir, "pjarczak_bambu_linux_host");
263
- return fs.existsSync(host) ? quoteShell(host) : undefined;
313
+ return fs.existsSync(host) ? `${quoteShell(host)} | cat` : undefined;
264
314
  }
265
315
  return undefined;
266
316
  }
@@ -301,6 +351,7 @@ async function runBridgeRequests(command, requests, timeoutMs) {
301
351
  return await new Promise((resolve, reject) => {
302
352
  const child = spawn(command, {
303
353
  shell: true,
354
+ env: { ...process.env, ...linuxPluginLibraryEnv(process.env, process.platform) },
304
355
  stdio: ["pipe", "pipe", "pipe"],
305
356
  });
306
357
  const stdoutChunks = [];
@@ -314,12 +365,40 @@ async function runBridgeRequests(command, requests, timeoutMs) {
314
365
  clearTimeout(timer);
315
366
  callback();
316
367
  };
368
+ const unanswered = new Set(requests.map((request) => request.id));
317
369
  const timer = setTimeout(() => {
370
+ child.stdin?.end();
318
371
  child.kill("SIGKILL");
319
- settle(() => reject(new Error(`FULU bridge request timed out after ${timeoutMs}ms.`)));
372
+ // Keep what the host did say: a hung host is diagnosed from its stderr and partial answers.
373
+ const responses = Object.fromEntries(parseBridgeFrames(Buffer.concat(stdoutChunks))
374
+ .filter((frame) => frame.type === FRAME_TYPE_JSON_RESPONSE)
375
+ .map((frame) => [String(frame.id), (() => { try {
376
+ return JSON.parse(frame.payload.toString("utf8"));
377
+ }
378
+ catch {
379
+ return null;
380
+ } })()]));
381
+ const stderr = Buffer.concat(stderrChunks).toString("utf8").trim().slice(-1000);
382
+ const hints = bridgeHints(null, responses);
383
+ settle(() => reject(new Error(`FULU bridge request timed out after ${timeoutMs}ms with ${unanswered.size} of ${requests.length} request(s) unanswered.` +
384
+ (stderr ? ` Host stderr: ${stderr}` : "") + (hints.length ? ` ${hints.join(" ")}` : ""))));
320
385
  }, timeoutMs);
321
- child.stdout?.on("data", (chunk) => stdoutChunks.push(chunk));
386
+ // FULU's host exits on stdin EOF without answering requests still in
387
+ // flight, so stdin closes only once every request has a response.
388
+ child.stdout?.on("data", (chunk) => {
389
+ stdoutChunks.push(chunk);
390
+ if (!unanswered.size)
391
+ return;
392
+ for (const frame of parseBridgeFrames(Buffer.concat(stdoutChunks))) {
393
+ if (frame.type === FRAME_TYPE_JSON_RESPONSE)
394
+ unanswered.delete(frame.id);
395
+ }
396
+ if (!unanswered.size)
397
+ child.stdin?.end();
398
+ });
322
399
  child.stderr?.on("data", (chunk) => stderrChunks.push(chunk));
400
+ // A host that exits early (exit code 101) closes the pipe; close reports the result.
401
+ child.stdin?.on("error", () => { });
323
402
  child.on("error", (error) => {
324
403
  settle(() => reject(error));
325
404
  });
@@ -357,7 +436,8 @@ async function runBridgeRequests(command, requests, timeoutMs) {
357
436
  for (const request of requests) {
358
437
  child.stdin?.write(writeJsonRequestFrame(request.id, request.method, request.payload));
359
438
  }
360
- child.stdin?.end();
439
+ if (!unanswered.size)
440
+ child.stdin?.end();
361
441
  });
362
442
  }
363
443
  async function probeFuluBridge(command, timeoutMs) {
@@ -366,9 +446,13 @@ async function probeFuluBridge(command, timeoutMs) {
366
446
  { id: 2, method: "bridge.capabilities", payload: {} },
367
447
  { id: 3, method: "bridge.runtime_info", payload: {} },
368
448
  ], timeoutMs);
449
+ const handshake = probe.responses["1"];
450
+ const hints = bridgeHints(probe.exitCode, probe.responses);
369
451
  return {
370
452
  exitCode: probe.exitCode,
371
- ok: probe.ok && Boolean(probe.responses["1"]),
453
+ // A handshake from a host that could not load the networking plug-in is not a ready bridge.
454
+ ok: probe.ok && Boolean(handshake) && handshake?.ok !== false && handshake?.network_loaded !== false,
455
+ ...(hints.length ? { hints } : {}),
372
456
  stderr: probe.stderr,
373
457
  logs: probe.logs,
374
458
  handshake: probe.responses["1"] || null,
@@ -400,6 +484,7 @@ export async function invokeFuluBridgeRpc(options) {
400
484
  }
401
485
  const command = options.bridgeCommand || process.env.FULU_BAMBU_BRIDGE_COMMAND || "";
402
486
  const bridgeResult = await runBridgeRequests(command, [{ id: 1, method, payload: options.payload || {} }], options.timeoutMs || 5000);
487
+ const hints = bridgeHints(bridgeResult.exitCode, bridgeResult.responses);
403
488
  return {
404
489
  method,
405
490
  readOnly,
@@ -408,6 +493,7 @@ export async function invokeFuluBridgeRpc(options) {
408
493
  bambuModel: options.bambuModel || undefined,
409
494
  exitCode: bridgeResult.exitCode,
410
495
  ok: bridgeResult.ok,
496
+ ...(hints.length ? { hints } : {}),
411
497
  stderr: bridgeResult.stderr,
412
498
  logs: bridgeResult.logs,
413
499
  response: bridgeResult.responses["1"] || null,
@@ -446,6 +532,12 @@ export async function inspectFuluOrcaSetup(options) {
446
532
  pluginDir: pluginDir || null,
447
533
  runtimeDir: runtimeDir || null,
448
534
  bridgeCommand: bridgeCommand || null,
535
+ // Linux only: absolute plug-in library paths passed to FULU's host (FULU #51).
536
+ ...(platform === "linux" ? { pluginLibraries: {
537
+ ...Object.fromEntries(["PJARCZAK_BAMBU_NETWORK_SO", "PJARCZAK_BAMBU_SOURCE_SO"]
538
+ .filter((name) => process.env[name]?.trim()).map((name) => [name, process.env[name].trim()])),
539
+ ...linuxPluginLibraryEnv(process.env, process.platform),
540
+ } } : {}),
449
541
  fileChecks,
450
542
  missingRequiredFiles,
451
543
  optionalMissingFiles,
@@ -471,7 +563,12 @@ export async function inspectFuluOrcaSetup(options) {
471
563
  "Bambu print start still enforces BAMBU_MODEL before generating or sending project G-code.",
472
564
  "Raw bridge print methods are disabled; print_3mf inspects the exact plate, checks the live printer and asks for human confirmation.",
473
565
  "The FULU BambuNetwork bridge is a separate runtime from the LAN MQTT/FTPS path; use bridge probing to verify that runtime before relying on cloud behavior.",
474
- "FULU macOS release bundles may ship the bridge dylib and host shims before the Linux plugin .so payload is present; missing linux-plugin entries mean the slicer can still be tested, but the BambuNetwork bridge is not fully ready.",
566
+ ...(platform === "darwin"
567
+ ? ["FULU macOS release bundles may ship the bridge dylib and host shims before the Linux plugin .so payload is present; missing linux-plugin entries mean the slicer can still be tested, but the BambuNetwork bridge is not fully ready."]
568
+ : []),
569
+ ...(platform === "linux"
570
+ ? ["FULU's v1.0.0 Linux host exits 101 when its stdout is a Node.js socket (FULU #50) and opens the plug-in libraries from its working directory (FULU #51). Pipe the host through cat and set PJARCZAK_BAMBU_PLUGIN_DIR; the server then passes the absolute library paths."]
571
+ : []),
475
572
  ],
476
573
  };
477
574
  }
package/dist/index.js CHANGED
@@ -220,11 +220,11 @@ const SLICE_TOOL_PROPERTIES = {
220
220
  description: "Template directory override when resolving template_name (default: BAMBU_TEMPLATE_DIR or ~/Sync/bambu/templates).",
221
221
  },
222
222
  uptodate: { type: "boolean", description: "Bambu-compatible slicing: refresh 3MF preset configs to the installed slicer version (--uptodate)." },
223
- repetitions: { type: "number", description: "Bambu-compatible slicing: print N identical copies (--repetitions)." },
223
+ repetitions: { type: "number", description: "Bambu-compatible slicing: print N identical copies (--repetitions). OrcaSlicer copies an STL with --clone-objects; do not combine with clone_objects." },
224
224
  orient: { type: "boolean", description: "Bambu-compatible slicing: auto-orient for printability (--orient)." },
225
- arrange: { type: "boolean", description: "Bambu-compatible slicing: auto-arrange objects on the plate (--arrange). Set false to keep an existing layout." },
225
+ arrange: { type: "boolean", description: "Bambu-compatible slicing: auto-arrange objects on the plate (--arrange); STL input is arranged by default. Set false to keep an existing layout: an STL that is not already on the plate then fails to slice." },
226
226
  ensure_on_bed: { type: "boolean", description: "Bambu-compatible slicing: lower floating models onto the bed (--ensure-on-bed)." },
227
- clone_objects: { type: "string", description: "Bambu-compatible slicing: comma-separated clone counts per object index, e.g. '1,3,1,10'." },
227
+ clone_objects: { type: "string", description: "Bambu-compatible slicing: clone the loaded model, total copies as a single integer, e.g. '3' (--clone-objects). The Bambu-style CLIs count copies per input file and this tool loads one file, so a comma-separated list is rejected by the slicer." },
228
228
  skip_objects: { type: "string", description: "Bambu-compatible slicing: comma-separated object indices to skip, e.g. '3,5,10'." },
229
229
  enable_timelapse: { type: "boolean", description: "Bambu-compatible slicing: insert timelapse parking moves (--enable-timelapse)." },
230
230
  allow_mix_temp: { type: "boolean", description: "Bambu-compatible slicing: allow filaments with different temperature requirements on one plate." },
@@ -8,7 +8,7 @@ import { BambuClient, PushAllCommand } from "bambu-node";
8
8
  import { MAX_PRINT_FILE_BYTES, readSafe3mfArchive } from "../safety/archive.js";
9
9
  import { inspectPrintFile } from "../safety/print-file.js";
10
10
  import { normalizeModel, validateTemperature } from "../safety/limits.js";
11
- import { manualHeatingRequirements, publishBambuCommand, readFreshPrinterStatus, validatePrinterState, } from "../safety/printer-state.js";
11
+ import { manualHeatingRequirements, publishBambuCommand, readFreshPrinterStatus, unchippedNozzles, validatePrinterState, } from "../safety/printer-state.js";
12
12
  import { cancelPendingPrinterOperations, normalizedRemotePath, uniquePrintName, withPrinterOperation, withPrintSnapshot, } from "../safety/artifact.js";
13
13
  import { requireHumanConfirmation } from "../safety/confirmation.js";
14
14
  import { installBambuNodeRejectionGuard } from "./bambu-node-guard.js";
@@ -118,10 +118,13 @@ export class BambuImplementation extends PrinterImplementation {
118
118
  }
119
119
  async confirmPrintPreflight(serial, status, inspection) {
120
120
  const finishedJob = this.finishedJobIdentity(status);
121
+ const unchipped = unchippedNozzles(status?.raw);
121
122
  await this.confirmHardwareAction(`Start a checked print on ${inspection.model.toUpperCase()} (${serial})? Nozzles: ${inspection.nozzleDiameters.join(", ")} mm. ` +
122
123
  `Materials: ${inspection.materials.join(", ")}. Peak targets: nozzle ${inspection.maxNozzleTemperature}°C, bed ${inspection.maxBedTemperature}°C, chamber ${inspection.maxChamberTemperature}°C. ` +
123
124
  `File SHA-256: ${inspection.sha256}. Confirm the physical spool labels and that the build plate is clear.` +
124
- (finishedJob !== undefined ? " The printer reports FINISH: remove the previous part and debris before confirming." : ""), finishedJob !== undefined);
125
+ (finishedJob !== undefined ? " The printer reports FINISH: remove the previous part and debris before confirming." : "") +
126
+ (unchipped.length ? ` Printer ${unchipped.length === 1 ? `nozzle ${unchipped[0]} reports` : `nozzles ${unchipped.join(" and ")} report`} no identity chip, ` +
127
+ `so ${unchipped.length === 1 ? "its" : "their"} diameter and type are the printer's configured settings; confirm they match the installed ${unchipped.length === 1 ? "hotend" : "hotends"}.` : ""), finishedJob !== undefined);
125
128
  return finishedJob;
126
129
  }
127
130
  assertBedClearance(status, confirmedFinishedJob) {
@@ -180,8 +183,12 @@ export class BambuImplementation extends PrinterImplementation {
180
183
  catch (error) {
181
184
  console.warn("PushAllCommand failed, continuing with cached status", error);
182
185
  }
183
- if (!printer.data || Object.keys(printer.data).length === 0) {
184
- await new Promise((resolve) => setTimeout(resolve, 1500));
186
+ // The first push after connecting can be a sparse hello (model and
187
+ // modules); the full status follows. Returning before it merges reported
188
+ // status UNKNOWN, so wait for gcode_state within the same 1.5 s bound.
189
+ const deadline = Date.now() + 1500;
190
+ while (typeof printer.data?.gcode_state !== "string" && Date.now() < deadline) {
191
+ await sleep(50);
185
192
  }
186
193
  const data = printer.data;
187
194
  return {
@@ -37,7 +37,14 @@ export const CONFIRMATION_UNAVAILABLE = "Human hardware confirmation requires an
37
37
  export async function requireHumanConfirmation(confirm, message, scope, physicalCheck = false) {
38
38
  if (!physicalCheck && confirmationOptOut(scope))
39
39
  return;
40
- if (!confirm || !(await confirm(message))) {
41
- throw new Error("Hardware safety confirmation was declined or unavailable. Use an MCP client with elicitation support. No command was sent.");
40
+ if (!confirm)
41
+ throw new Error(CONFIRMATION_UNAVAILABLE);
42
+ if (!(await confirm(message))) {
43
+ throw new Error("Hardware safety confirmation was declined or cancelled in the MCP client. No command was sent. " + (physicalCheck
44
+ ? "This confirmation covers a physical check at the printer and always needs a person to accept it."
45
+ : "Some clients decline prompts automatically, for example in a non-interactive permission mode. A deliberately headless " +
46
+ `setup that approves jobs another way can set ${scope === "bambu"
47
+ ? "PRINT_REQUIRE_CONFIRMATION=0 (or BAMBU_REQUIRE_CONFIRMATION=0)"
48
+ : "PRINT_REQUIRE_CONFIRMATION=0"} to skip ordinary print and heating prompts.`));
42
49
  }
43
50
  }
@@ -44,6 +44,8 @@ export interface ValidatedPrinterState {
44
44
  reportedNozzles: ReportedNozzle[];
45
45
  filaments: ReportedFilament[];
46
46
  }
47
+ /** Nozzles whose reported diameter and type are configured settings rather than chip readings. */
48
+ export declare function unchippedNozzles(raw: unknown): number[];
47
49
  /** Verifies reported configuration, not a physical nozzle/spool inspection. */
48
50
  export declare function validatePrinterState(status: any, requirements: PrinterStateRequirements): ValidatedPrinterState;
49
51
  /** Manual M104 cannot remap filament slots. Validate the material already at the nozzle. */
@@ -138,6 +138,41 @@ function nozzleTypeAndFlow(value) {
138
138
  const type = token(value);
139
139
  return { type: ["stainlesssteel", "hardenedsteel", "tungstencarbide", "brass"].includes(type) ? type : undefined };
140
140
  }
141
+ /**
142
+ * A nozzle without an identity chip reports serial "N/A" and keeps the
143
+ * unreliable bit set permanently (stock H2D HS01 nozzles), so its diameter
144
+ * and type are configured settings, as on single-nozzle printers.
145
+ */
146
+ function hasNoIdentityChip(entry) {
147
+ return typeof entry?.sn === "string" && entry.sn.trim().toUpperCase() === "N/A";
148
+ }
149
+ /** Bit 0 (information unreliable) is accepted only from a nozzle without an identity chip. */
150
+ function unreliableWithoutChip(entry, stat) {
151
+ return (stat & 1) !== 0 && hasNoIdentityChip(entry);
152
+ }
153
+ /** Nozzles whose reported diameter and type are configured settings rather than chip readings. */
154
+ export function unchippedNozzles(raw) {
155
+ const info = raw?.device?.nozzle?.info;
156
+ if (!Array.isArray(info))
157
+ return [];
158
+ return info.flatMap(entry => {
159
+ const index = finiteNumber(entry?.id);
160
+ const stat = finiteNumber(entry?.stat);
161
+ return index !== undefined && Number.isInteger(index) && index >= 0 && index < 16 &&
162
+ stat !== undefined && Number.isInteger(stat) && stat >= 0 && unreliableWithoutChip(entry, stat) ? [index] : [];
163
+ });
164
+ }
165
+ const loggedUnchippedNozzles = new Set();
166
+ /** Name each unchipped nozzle a check accepted once per printer on stderr, so headless runs still record it. */
167
+ function logUnchippedNozzles(raw, serial) {
168
+ for (const index of unchippedNozzles(raw)) {
169
+ const key = `${serial}:${index}`;
170
+ if (loggedUnchippedNozzles.has(key))
171
+ continue;
172
+ loggedUnchippedNozzles.add(key);
173
+ console.error(`Printer ${serial || "(unknown serial)"} nozzle ${index} reports no identity chip; its diameter and type are the printer's configured settings.`);
174
+ }
175
+ }
141
176
  function reportedNozzles(raw) {
142
177
  const info = raw.device?.nozzle?.info;
143
178
  const output = [];
@@ -151,9 +186,13 @@ function reportedNozzles(raw) {
151
186
  if (index === undefined || !Number.isInteger(index) || index < 0 || diameter === undefined || diameter <= 0)
152
187
  throw new Error("Printer reported malformed nozzle identity or diameter.");
153
188
  if (entry.stat !== undefined) {
189
+ // BambuStudio DevNozzle: bit 0 set means the information is unreliable;
190
+ // bits 1-2 are 0 normal, 1 abnormal, 2 unknown.
154
191
  const stat = finiteNumber(entry.stat);
155
- if (stat === undefined || !Number.isInteger(stat) || stat < 0 || (stat & 7) !== 0)
192
+ if (stat === undefined || !Number.isInteger(stat) || stat < 0 || (stat & 6) !== 0)
156
193
  throw new Error(`Printer nozzle ${index} information is unreliable, abnormal or unknown.`);
194
+ if ((stat & 1) !== 0 && !unreliableWithoutChip(entry, stat))
195
+ throw new Error(`Printer nozzle ${index} information is unreliable; refresh the nozzle information on the printer.`);
157
196
  }
158
197
  if (output.some(nozzle => nozzle.index === index))
159
198
  throw new Error("Printer reported ambiguous duplicate nozzle indices.");
@@ -338,6 +377,8 @@ export function validatePrinterState(status, requirements) {
338
377
  filaments.push({ position, material, trayIndex, nozzleMin, nozzleMax });
339
378
  }
340
379
  }
380
+ if (nozzles.length)
381
+ logUnchippedNozzles(raw, identity.observedSerial ?? String(status.serial ?? ""));
341
382
  return { model, reportedNozzles: nozzles, filaments };
342
383
  }
343
384
  /** Manual M104 cannot remap filament slots. Validate the material already at the nozzle. */
@@ -100,8 +100,28 @@ export declare class BambuCliProfilePreparer {
100
100
  */
101
101
  private isolateMachineSettings;
102
102
  }
103
+ /** How the CLI receives the model: Orca STL transforms are applied to a mesh copy first. */
104
+ export interface BambuCliInput {
105
+ slicerType: BambuCliSlicerType;
106
+ is3mf: boolean;
107
+ /** scale and rotations are already applied to the mesh passed to the CLI. */
108
+ meshTransformed: boolean;
109
+ }
110
+ /**
111
+ * Decide how the CLI receives the model. OrcaSlicer's CLI (upstream and FULU)
112
+ * crashes with SIGSEGV in PartPlate::check_outside, which needs the GUI's
113
+ * Plater, when --scale or --rotate-x/-y move an object below the bed while it
114
+ * arranges, the default for STL input. An Orca STL's transforms are therefore
115
+ * applied to a mesh copy; combinations the CLI cannot handle are refused.
116
+ */
117
+ export declare function planBambuCliInput(slicerType: BambuCliSlicerType, is3mf: boolean, options: BambuSliceOptions): BambuCliInput;
118
+ /**
119
+ * The CLI's own reason for a failed run: result.json in the run's fresh output
120
+ * directory (written on Linux for handled errors), else the exit-code table.
121
+ */
122
+ export declare function bambuCliFailureReason(exitCode: number | null, outputDir: string): string | undefined;
103
123
  /** CLI flags for the prepared bundle and caller options (input path appended by the caller). */
104
- export declare function buildBambuCliArgs(bundle: BambuSettingsBundle, outputDir: string, exportName: string, options: BambuSliceOptions): string[];
124
+ export declare function buildBambuCliArgs(bundle: BambuSettingsBundle, outputDir: string, exportName: string, options: BambuSliceOptions, input: BambuCliInput): string[];
105
125
  /**
106
126
  * A sliced Bambu project must contain at least one nonempty
107
127
  * Metadata/plate_<n>.gcode entry. A .gcode.md5 checksum or the unsliced
@@ -469,8 +469,76 @@ const PROFILE_METADATA_KEYS = new Set([
469
469
  "name", "type", "from", "inherits", "instantiation", "setting_id", "version", "is_custom_defined",
470
470
  "compatible_printers", "compatible_printers_condition", "compatible_prints", "compatible_prints_condition",
471
471
  ]);
472
+ /**
473
+ * Decide how the CLI receives the model. OrcaSlicer's CLI (upstream and FULU)
474
+ * crashes with SIGSEGV in PartPlate::check_outside, which needs the GUI's
475
+ * Plater, when --scale or --rotate-x/-y move an object below the bed while it
476
+ * arranges, the default for STL input. An Orca STL's transforms are therefore
477
+ * applied to a mesh copy; combinations the CLI cannot handle are refused.
478
+ */
479
+ export function planBambuCliInput(slicerType, is3mf, options) {
480
+ const input = { slicerType, is3mf, meshTransformed: false };
481
+ if (slicerType === "bambustudio")
482
+ return input;
483
+ const tilts = !!options.rotateX || !!options.rotateY;
484
+ if (is3mf) {
485
+ if (options.arrange === true && ((options.scale ?? 1) > 1 || tilts)) {
486
+ throw new Error("OrcaSlicer's CLI crashes when scale above 1, rotate_x or rotate_y moves a 3MF object below the bed while arranging. " +
487
+ "Set arrange to false, or apply the transform in the slicer and export the project.");
488
+ }
489
+ return input;
490
+ }
491
+ if (options.repetitions !== undefined && options.cloneObjects) {
492
+ throw new Error("Use either repetitions or clone_objects for an STL with OrcaSlicer, not both.");
493
+ }
494
+ return { ...input, meshTransformed: options.scale !== undefined || tilts };
495
+ }
496
+ /**
497
+ * Bambu Studio and OrcaSlicer CLI errors (libslic3r/Utils.hpp). The process
498
+ * exits with the low byte of the code; on Linux result.json names the error.
499
+ */
500
+ const BAMBU_CLI_ERRORS = {
501
+ [-2]: "invalid parameters to the slicer",
502
+ [-3]: "input files not found",
503
+ [-5]: "preset file cannot be parsed",
504
+ [-6]: "model file cannot be parsed",
505
+ [-9]: "copying objects failed",
506
+ [-17]: "printer is not compatible with the process preset",
507
+ [-21]: "auto-arranging objects failed",
508
+ [-22]: "auto-orienting objects failed",
509
+ [-50]: "a plate is empty or has no object fully inside it; arrange the model or place it on the plate",
510
+ [-51]: "incorrect slicing parameters",
511
+ [-52]: "objects extend over the bed boundary",
512
+ [-60]: "no printable objects remain after skipping",
513
+ [-61]: "filaments are not compatible with the plate type",
514
+ [-62]: "the filaments' temperatures differ too much",
515
+ [-63]: "objects collide in print-by-object mode",
516
+ [-64]: "objects collide",
517
+ [-100]: "slicing the model failed",
518
+ [-101]: "G-code conflicts after slicing",
519
+ [-102]: "G-code in an unprintable area",
520
+ };
521
+ /**
522
+ * The CLI's own reason for a failed run: result.json in the run's fresh output
523
+ * directory (written on Linux for handled errors), else the exit-code table.
524
+ */
525
+ export function bambuCliFailureReason(exitCode, outputDir) {
526
+ try {
527
+ const result = JSON.parse(fs.readFileSync(path.join(outputDir, "result.json"), "utf8"));
528
+ if (typeof result?.error_string === "string" && result.error_string.trim()) {
529
+ return `${result.error_string.trim()}${result.return_code !== undefined ? ` [${result.return_code}]` : ""}`;
530
+ }
531
+ }
532
+ catch {
533
+ // result.json is written only on Linux and only for handled errors.
534
+ }
535
+ if (exitCode === null)
536
+ return undefined;
537
+ const code = Object.keys(BAMBU_CLI_ERRORS).map(Number).find((value) => value === exitCode || (value & 0xff) === exitCode);
538
+ return code === undefined ? undefined : `${BAMBU_CLI_ERRORS[code]} [${code}]`;
539
+ }
472
540
  /** CLI flags for the prepared bundle and caller options (input path appended by the caller). */
473
- export function buildBambuCliArgs(bundle, outputDir, exportName, options) {
541
+ export function buildBambuCliArgs(bundle, outputDir, exportName, options, input) {
474
542
  const args = [
475
543
  "--slice", String(options.slicePlate ?? 0),
476
544
  "--outputdir", outputDir,
@@ -496,16 +564,23 @@ export function buildBambuCliArgs(bundle, outputDir, exportName, options) {
496
564
  args.push("--orient", options.orient ? "1" : "0");
497
565
  if (options.arrange !== undefined)
498
566
  args.push("--arrange", options.arrange ? "1" : "0");
499
- if (options.repetitions !== undefined)
500
- args.push("--repetitions", String(options.repetitions));
501
- if (options.scale !== undefined)
502
- args.push("--scale", String(options.scale));
503
- if (options.rotate !== undefined)
504
- args.push("--rotate", String(options.rotate));
505
- if (options.rotateX !== undefined)
506
- args.push("--rotate-x", String(options.rotateX));
507
- if (options.rotateY !== undefined)
508
- args.push("--rotate-y", String(options.rotateY));
567
+ if (options.repetitions !== undefined) {
568
+ // OrcaSlicer rejects --repetitions for one STL; it copies the loaded model with --clone-objects.
569
+ if (input.slicerType !== "bambustudio" && !input.is3mf)
570
+ args.push("--clone-objects", String(options.repetitions));
571
+ else
572
+ args.push("--repetitions", String(options.repetitions));
573
+ }
574
+ if (!input.meshTransformed) {
575
+ if (options.scale !== undefined)
576
+ args.push("--scale", String(options.scale));
577
+ if (options.rotate !== undefined)
578
+ args.push("--rotate", String(options.rotate));
579
+ if (options.rotateX !== undefined)
580
+ args.push("--rotate-x", String(options.rotateX));
581
+ if (options.rotateY !== undefined)
582
+ args.push("--rotate-y", String(options.rotateY));
583
+ }
509
584
  if (options.cloneObjects)
510
585
  args.push("--clone-objects", options.cloneObjects);
511
586
  if (options.skipObjects)
@@ -5,6 +5,8 @@ export interface SlicerRunOptions {
5
5
  slicerType: string;
6
6
  /** Polled while the slicer runs; returning true kills it. */
7
7
  isCancelled?: () => boolean;
8
+ /** The slicer's own reason for a nonzero exit, read before its output directory is removed. */
9
+ failureReason?: (exitCode: number | null) => string | undefined;
8
10
  }
9
11
  export interface SlicerRunResult {
10
12
  stdoutTail: string;