mcp-3d-printer-server 1.2.9 → 1.2.11

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 (86) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/CONTRIBUTORS.md +49 -0
  3. package/README.md +528 -811
  4. package/dist/3mf_parser.js +8 -8
  5. package/dist/blender-mcp-bridge.d.ts +1 -0
  6. package/dist/blender-mcp-bridge.js +209 -21
  7. package/dist/fulu-orca.js +29 -5
  8. package/dist/index.js +614 -110
  9. package/dist/printers/bambu-node-guard.d.ts +2 -0
  10. package/dist/printers/bambu-node-guard.js +38 -0
  11. package/dist/printers/bambu.d.ts +53 -7
  12. package/dist/printers/bambu.js +420 -126
  13. package/dist/printers/creality.d.ts +15 -6
  14. package/dist/printers/creality.js +42 -9
  15. package/dist/printers/duet.d.ts +11 -6
  16. package/dist/printers/duet.js +57 -34
  17. package/dist/printers/generic-base.d.ts +69 -0
  18. package/dist/printers/generic-base.js +209 -0
  19. package/dist/printers/klipper.d.ts +17 -6
  20. package/dist/printers/klipper.js +63 -16
  21. package/dist/printers/octoprint.d.ts +18 -6
  22. package/dist/printers/octoprint.js +64 -9
  23. package/dist/printers/printer-factory.d.ts +11 -2
  24. package/dist/printers/printer-factory.js +9 -8
  25. package/dist/printers/prusa.d.ts +14 -6
  26. package/dist/printers/prusa.js +56 -9
  27. package/dist/printers/repetier.d.ts +18 -6
  28. package/dist/printers/repetier.js +48 -6
  29. package/dist/safety/archive.d.ts +11 -0
  30. package/dist/safety/archive.js +214 -0
  31. package/dist/safety/artifact.d.ts +6 -0
  32. package/dist/safety/artifact.js +66 -0
  33. package/dist/safety/confirmation.d.ts +22 -0
  34. package/dist/safety/confirmation.js +43 -0
  35. package/dist/safety/expected-peaks.d.ts +24 -0
  36. package/dist/safety/expected-peaks.js +24 -0
  37. package/dist/safety/gcode-thermal.d.ts +64 -0
  38. package/dist/safety/gcode-thermal.js +382 -0
  39. package/dist/safety/limits.d.ts +31 -0
  40. package/dist/safety/limits.js +105 -0
  41. package/dist/safety/print-file.d.ts +25 -0
  42. package/dist/safety/print-file.js +517 -0
  43. package/dist/safety/printer-state.d.ts +51 -0
  44. package/dist/safety/printer-state.js +365 -0
  45. package/dist/slicer/bambu-cli.d.ts +110 -0
  46. package/dist/slicer/bambu-cli.js +547 -0
  47. package/dist/slicer/profile-flatten.d.ts +104 -0
  48. package/dist/slicer/profile-flatten.js +676 -0
  49. package/dist/slicer/run-slicer.d.ts +18 -0
  50. package/dist/slicer/run-slicer.js +102 -0
  51. package/dist/slicer/slicer-error.d.ts +27 -0
  52. package/dist/slicer/slicer-error.js +53 -0
  53. package/dist/slicer/templates.d.ts +126 -0
  54. package/dist/slicer/templates.js +255 -0
  55. package/dist/stl/stl-manipulator.d.ts +25 -3
  56. package/dist/stl/stl-manipulator.js +214 -232
  57. package/package.json +16 -9
  58. package/server.json +29 -0
  59. package/src/3mf_parser.ts +8 -8
  60. package/src/blender-mcp-bridge.ts +200 -19
  61. package/src/fulu-orca.ts +36 -6
  62. package/src/index.ts +686 -137
  63. package/src/printers/bambu-node-guard.ts +44 -0
  64. package/src/printers/bambu.ts +458 -164
  65. package/src/printers/creality.ts +40 -9
  66. package/src/printers/duet.ts +64 -53
  67. package/src/printers/generic-base.ts +286 -0
  68. package/src/printers/klipper.ts +83 -28
  69. package/src/printers/octoprint.ts +74 -21
  70. package/src/printers/printer-factory.ts +21 -11
  71. package/src/printers/prusa.ts +53 -10
  72. package/src/printers/repetier.ts +55 -17
  73. package/src/safety/archive.ts +181 -0
  74. package/src/safety/artifact.ts +58 -0
  75. package/src/safety/confirmation.ts +56 -0
  76. package/src/safety/expected-peaks.ts +47 -0
  77. package/src/safety/gcode-thermal.ts +404 -0
  78. package/src/safety/limits.ts +98 -0
  79. package/src/safety/print-file.ts +354 -0
  80. package/src/safety/printer-state.ts +339 -0
  81. package/src/slicer/bambu-cli.ts +633 -0
  82. package/src/slicer/profile-flatten.ts +830 -0
  83. package/src/slicer/run-slicer.ts +126 -0
  84. package/src/slicer/slicer-error.ts +72 -0
  85. package/src/slicer/templates.ts +288 -0
  86. package/src/stl/stl-manipulator.ts +247 -275
@@ -1,48 +1,95 @@
1
- import { PrinterImplementation } from "../types.js";
2
1
  import fs from "fs";
3
2
  import FormData from "form-data";
4
- export class KlipperImplementation extends PrinterImplementation {
3
+ import { GenericPrinterImplementation } from "./generic-base.js";
4
+ function encodePath(filename) {
5
+ return filename.replace(/^\/+/, "").split("/").map(encodeURIComponent).join("/");
6
+ }
7
+ export class KlipperImplementation extends GenericPrinterImplementation {
8
+ constructor() {
9
+ super(...arguments);
10
+ this.printerLabel = "Klipper/Moonraker";
11
+ }
12
+ authHeaders(apiKey) {
13
+ return apiKey ? { headers: { "X-Api-Key": apiKey } } : {};
14
+ }
5
15
  async getStatus(host, port, apiKey) {
6
16
  const url = `http://${host}:${port}/printer/info`;
7
- const response = await this.apiClient.get(url);
17
+ const response = await this.apiClient.get(url, this.authHeaders(apiKey));
8
18
  return response.data;
9
19
  }
10
20
  async getFiles(host, port, apiKey) {
11
21
  const url = `http://${host}:${port}/server/files/list`;
12
- const response = await this.apiClient.get(url);
22
+ const response = await this.apiClient.get(url, this.authHeaders(apiKey));
13
23
  return response.data;
14
24
  }
15
25
  async getFile(host, port, apiKey, filename) {
16
26
  const url = `http://${host}:${port}/server/files/metadata?filename=${encodeURIComponent(filename)}`;
17
- const response = await this.apiClient.get(url);
27
+ const response = await this.apiClient.get(url, this.authHeaders(apiKey));
18
28
  return response.data;
19
29
  }
20
- async uploadFile(host, port, apiKey, filePath, filename, print) {
30
+ /**
31
+ * Moonraker object query: webhooks.state must be "ready" (not startup,
32
+ * shutdown or error) and print_stats.state standby/complete/cancelled.
33
+ * complete/cancelled can leave a part on the bed, so they need a human.
34
+ */
35
+ async readPrinterState(host, port, apiKey) {
36
+ let data;
37
+ try {
38
+ data = (await this.apiClient.get(`http://${host}:${port}/printer/objects/query?webhooks&print_stats`, this.authHeaders(apiKey))).data;
39
+ }
40
+ catch (error) {
41
+ throw new Error(`Cannot read Klipper/Moonraker printer state; nothing was sent: ${error.message}`);
42
+ }
43
+ const status = data?.result?.status;
44
+ const klippy = status?.webhooks?.state;
45
+ const job = status?.print_stats?.state;
46
+ if (typeof klippy !== "string" || typeof job !== "string") {
47
+ throw new Error("Moonraker did not report webhooks.state and print_stats.state; cannot verify the printer is idle.");
48
+ }
49
+ const state = `klippy ${klippy}, print ${job}`;
50
+ if (klippy !== "ready") {
51
+ return { state, ready: false, reason: status?.webhooks?.state_message ? String(status.webhooks.state_message) : "Klipper is not ready" };
52
+ }
53
+ if (!["standby", "complete", "cancelled"].includes(job)) {
54
+ return { state, ready: false, reason: `print_stats.state is ${job}` };
55
+ }
56
+ return {
57
+ state,
58
+ ready: true,
59
+ finishedJob: job === "standby" ? undefined : JSON.stringify([job, status?.print_stats?.filename ?? null]),
60
+ };
61
+ }
62
+ async downloadRemoteFile(host, port, apiKey, filename, destination) {
63
+ await this.downloadToFile(`http://${host}:${port}/server/files/gcodes/${encodePath(filename)}`, apiKey ? { "X-Api-Key": apiKey } : {}, destination);
64
+ }
65
+ /** Current Moonraker returns {result:{item}}; older releases returned result "success". */
66
+ uploadSucceeded(data) {
67
+ return data?.result === "success" || !!data?.result?.item || !!data?.item;
68
+ }
69
+ async rawUploadFile(host, port, apiKey, filePath, filename, print) {
21
70
  const url = `http://${host}:${port}/server/files/upload`;
22
71
  const formData = new FormData();
23
72
  formData.append("file", fs.createReadStream(filePath));
24
73
  formData.append("filename", filename);
25
74
  const response = await this.apiClient.post(url, formData, {
26
75
  headers: {
27
- ...formData.getHeaders()
76
+ ...formData.getHeaders(),
77
+ ...(apiKey ? { "X-Api-Key": apiKey } : {})
28
78
  }
29
79
  });
30
- if (print && response.data.result === "success") {
31
- await this.startJob(host, port, apiKey, filename);
32
- }
33
80
  return response.data;
34
81
  }
35
- async startJob(host, port, apiKey, filename) {
82
+ async rawStartJob(host, port, apiKey, filename) {
36
83
  const url = `http://${host}:${port}/printer/print/start`;
37
- const response = await this.apiClient.post(url, { filename });
84
+ const response = await this.apiClient.post(url, { filename }, this.authHeaders(apiKey));
38
85
  return response.data;
39
86
  }
40
- async cancelJob(host, port, apiKey) {
87
+ async rawCancelJob(host, port, apiKey) {
41
88
  const url = `http://${host}:${port}/printer/print/cancel`;
42
- const response = await this.apiClient.post(url, null);
89
+ const response = await this.apiClient.post(url, null, this.authHeaders(apiKey));
43
90
  return response.data;
44
91
  }
45
- async setTemperature(host, port, apiKey, component, temperature) {
92
+ async rawSetTemperature(host, port, apiKey, component, temperature) {
46
93
  const url = `http://${host}:${port}/printer/gcode/script`;
47
94
  let gcode;
48
95
  if (component === "bed") {
@@ -56,7 +103,7 @@ export class KlipperImplementation extends PrinterImplementation {
56
103
  }
57
104
  const response = await this.apiClient.post(url, {
58
105
  script: gcode
59
- });
106
+ }, this.authHeaders(apiKey));
60
107
  return response.data;
61
108
  }
62
109
  }
@@ -1,10 +1,22 @@
1
- import { PrinterImplementation } from "../types.js";
2
- export declare class OctoPrintImplementation extends PrinterImplementation {
1
+ import { GenericPrinterImplementation, type GenericPrinterState, type HeaterComponent } from "./generic-base.js";
2
+ export declare class OctoPrintImplementation extends GenericPrinterImplementation {
3
+ protected readonly printerLabel = "OctoPrint";
4
+ protected readonly uploadStartsPrint = true;
3
5
  getStatus(host: string, port: string, apiKey: string): Promise<any>;
4
6
  getFiles(host: string, port: string, apiKey: string): Promise<any>;
5
7
  getFile(host: string, port: string, apiKey: string, filename: string): Promise<any>;
6
- uploadFile(host: string, port: string, apiKey: string, filePath: string, filename: string, print: boolean): Promise<any>;
7
- startJob(host: string, port: string, apiKey: string, filename: string): Promise<any>;
8
- cancelJob(host: string, port: string, apiKey: string): Promise<any>;
9
- setTemperature(host: string, port: string, apiKey: string, component: string, temperature: number): Promise<any>;
8
+ /**
9
+ * GET /api/printer state.flags (OctoPrint REST API). A 409 means the printer
10
+ * is not operational. Informational flags such as sdReady never block.
11
+ */
12
+ protected readPrinterState(host: string, port: string, apiKey: string): Promise<GenericPrinterState>;
13
+ /** OctoPrint reports effectivePrint=false when print=true had no effect. */
14
+ protected uploadStartedPrint(data: any): boolean;
15
+ protected downloadRemoteFile(host: string, port: string, apiKey: string, filename: string, destination: string): Promise<void>;
16
+ protected rawUploadFile(host: string, port: string, apiKey: string, filePath: string, filename: string, print: boolean): Promise<any>;
17
+ protected rawStartJob(host: string, port: string, apiKey: string, filename: string): Promise<any>;
18
+ protected rawCancelJob(host: string, port: string, apiKey: string): Promise<any>;
19
+ /** OctoPrint tool targets are keyed tool{n} (POST /api/printer/tool, command "target"). */
20
+ protected heaterComponent(component: string): HeaterComponent | undefined;
21
+ protected rawSetTemperature(host: string, port: string, apiKey: string, component: string, temperature: number): Promise<any>;
10
22
  }
@@ -1,7 +1,18 @@
1
- import { PrinterImplementation } from "../types.js";
2
1
  import fs from "fs";
3
2
  import FormData from "form-data";
4
- export class OctoPrintImplementation extends PrinterImplementation {
3
+ import { isAxiosError } from "axios";
4
+ import { GenericPrinterImplementation } from "./generic-base.js";
5
+ const BLOCKING_FLAGS = ["printing", "paused", "pausing", "cancelling", "resuming", "finishing", "error", "closedOrError"];
6
+ function encodePath(filename) {
7
+ return filename.replace(/^\/+/, "").split("/").map(encodeURIComponent).join("/");
8
+ }
9
+ export class OctoPrintImplementation extends GenericPrinterImplementation {
10
+ constructor() {
11
+ super(...arguments);
12
+ this.printerLabel = "OctoPrint";
13
+ // POST /api/files/local with print=true uploads and starts the same bytes.
14
+ this.uploadStartsPrint = true;
15
+ }
5
16
  async getStatus(host, port, apiKey) {
6
17
  const url = `http://${host}:${port}/api/printer`;
7
18
  const response = await this.apiClient.get(url, {
@@ -29,7 +40,40 @@ export class OctoPrintImplementation extends PrinterImplementation {
29
40
  });
30
41
  return response.data;
31
42
  }
32
- async uploadFile(host, port, apiKey, filePath, filename, print) {
43
+ /**
44
+ * GET /api/printer state.flags (OctoPrint REST API). A 409 means the printer
45
+ * is not operational. Informational flags such as sdReady never block.
46
+ */
47
+ async readPrinterState(host, port, apiKey) {
48
+ let data;
49
+ try {
50
+ data = (await this.apiClient.get(`http://${host}:${port}/api/printer`, { headers: { "X-Api-Key": apiKey } })).data;
51
+ }
52
+ catch (error) {
53
+ if (isAxiosError(error) && error.response?.status === 409) {
54
+ return { state: "Offline", ready: false, reason: "OctoPrint reports the printer is not operational" };
55
+ }
56
+ throw new Error(`Cannot read OctoPrint printer state; nothing was sent: ${error.message}`);
57
+ }
58
+ const flags = data?.state?.flags;
59
+ if (!flags || typeof flags !== "object") {
60
+ throw new Error("OctoPrint returned no printer state flags; cannot verify the printer is idle.");
61
+ }
62
+ const blocking = BLOCKING_FLAGS.filter((flag) => flags[flag] === true);
63
+ if (flags.operational !== true)
64
+ blocking.unshift("not operational");
65
+ if (flags.ready === false && !blocking.length)
66
+ blocking.push("not ready");
67
+ return { state: String(data?.state?.text ?? "unknown"), ready: blocking.length === 0, reason: blocking.join(", ") || undefined };
68
+ }
69
+ /** OctoPrint reports effectivePrint=false when print=true had no effect. */
70
+ uploadStartedPrint(data) {
71
+ return data?.effectivePrint !== false;
72
+ }
73
+ async downloadRemoteFile(host, port, apiKey, filename, destination) {
74
+ await this.downloadToFile(`http://${host}:${port}/downloads/files/local/${encodePath(filename)}`, { "X-Api-Key": apiKey }, destination);
75
+ }
76
+ async rawUploadFile(host, port, apiKey, filePath, filename, print) {
33
77
  const url = `http://${host}:${port}/api/files/local`;
34
78
  const formData = new FormData();
35
79
  formData.append("file", fs.createReadStream(filePath));
@@ -45,7 +89,7 @@ export class OctoPrintImplementation extends PrinterImplementation {
45
89
  });
46
90
  return response.data;
47
91
  }
48
- async startJob(host, port, apiKey, filename) {
92
+ async rawStartJob(host, port, apiKey, filename) {
49
93
  const url = `http://${host}:${port}/api/files/local/${filename}`;
50
94
  const response = await this.apiClient.post(url, {
51
95
  command: "select",
@@ -58,7 +102,7 @@ export class OctoPrintImplementation extends PrinterImplementation {
58
102
  });
59
103
  return response.data;
60
104
  }
61
- async cancelJob(host, port, apiKey) {
105
+ async rawCancelJob(host, port, apiKey) {
62
106
  const url = `http://${host}:${port}/api/job`;
63
107
  const response = await this.apiClient.post(url, {
64
108
  command: "cancel"
@@ -70,7 +114,19 @@ export class OctoPrintImplementation extends PrinterImplementation {
70
114
  });
71
115
  return response.data;
72
116
  }
73
- async setTemperature(host, port, apiKey, component, temperature) {
117
+ /** OctoPrint tool targets are keyed tool{n} (POST /api/printer/tool, command "target"). */
118
+ heaterComponent(component) {
119
+ const normalized = component.trim().toLowerCase();
120
+ if (normalized === "bed")
121
+ return { heater: "bed", raw: "bed" };
122
+ if (["extruder", "nozzle", "tool"].includes(normalized))
123
+ return { heater: "nozzle", raw: "tool0" };
124
+ const indexed = normalized.match(/^(?:extruder|tool)(\d+)$/);
125
+ if (indexed)
126
+ return { heater: "nozzle", raw: `tool${Number(indexed[1])}` };
127
+ return undefined;
128
+ }
129
+ async rawSetTemperature(host, port, apiKey, component, temperature) {
74
130
  let url = `http://${host}:${port}/api/printer/tool`;
75
131
  const data = {};
76
132
  if (component === "bed") {
@@ -78,10 +134,9 @@ export class OctoPrintImplementation extends PrinterImplementation {
78
134
  data.target = temperature;
79
135
  url = `http://${host}:${port}/api/printer/bed`;
80
136
  }
81
- else if (component.startsWith("extruder")) {
137
+ else if (/^tool\d+$/.test(component)) {
82
138
  data.command = "target";
83
- data.targets = {};
84
- data.targets[component] = temperature;
139
+ data.targets = { [component]: temperature };
85
140
  }
86
141
  else {
87
142
  throw new Error(`Unsupported component: ${component}`);
@@ -1,8 +1,17 @@
1
1
  import { PrinterImplementation } from "../types.js";
2
+ import type { ConfirmHardware } from "../safety/confirmation.js";
3
+ import type { PrintSafetyOptions } from "../safety/expected-peaks.js";
4
+ /** Every adapter accepts trailing safety options on its print and heating mutations. */
5
+ export type GuardedPrinterImplementation = PrinterImplementation & {
6
+ uploadFile(host: string, port: string, apiKey: string, filePath: string, filename: string, print: boolean, options?: PrintSafetyOptions): Promise<any>;
7
+ startJob(host: string, port: string, apiKey: string, filename: string, options?: PrintSafetyOptions): Promise<any>;
8
+ setTemperature(host: string, port: string, apiKey: string, component: string, temperature: unknown, options?: PrintSafetyOptions): Promise<any>;
9
+ };
2
10
  export declare class PrinterFactory {
3
11
  private implementations;
4
12
  private apiClient;
5
- constructor();
6
- getImplementation(type: string): PrinterImplementation;
13
+ /** confirm is the human (MCP elicitation) confirmation used before prints and positive heating. */
14
+ constructor(confirm?: ConfirmHardware);
15
+ getImplementation(type: string): GuardedPrinterImplementation;
7
16
  disconnectAll(): Promise<void>;
8
17
  }
@@ -7,16 +7,17 @@ import { PrusaImplementation } from "./prusa.js";
7
7
  import { CrealityImplementation } from "./creality.js";
8
8
  import axios from "axios";
9
9
  export class PrinterFactory {
10
- constructor() {
10
+ /** confirm is the human (MCP elicitation) confirmation used before prints and positive heating. */
11
+ constructor(confirm) {
11
12
  this.implementations = new Map();
12
13
  this.apiClient = axios.create({ timeout: 10000 });
13
- this.implementations.set("octoprint", new OctoPrintImplementation(this.apiClient));
14
- this.implementations.set("klipper", new KlipperImplementation(this.apiClient));
15
- this.implementations.set("duet", new DuetImplementation(this.apiClient));
16
- this.implementations.set("repetier", new RepetierImplementation(this.apiClient));
17
- this.implementations.set("bambu", new BambuImplementation(this.apiClient));
18
- this.implementations.set("prusa", new PrusaImplementation(this.apiClient));
19
- this.implementations.set("creality", new CrealityImplementation(this.apiClient));
14
+ this.implementations.set("octoprint", new OctoPrintImplementation(this.apiClient, confirm));
15
+ this.implementations.set("klipper", new KlipperImplementation(this.apiClient, confirm));
16
+ this.implementations.set("duet", new DuetImplementation(this.apiClient, confirm));
17
+ this.implementations.set("repetier", new RepetierImplementation(this.apiClient, confirm));
18
+ this.implementations.set("bambu", new BambuImplementation(this.apiClient, confirm));
19
+ this.implementations.set("prusa", new PrusaImplementation(this.apiClient, confirm));
20
+ this.implementations.set("creality", new CrealityImplementation(this.apiClient, confirm));
20
21
  }
21
22
  getImplementation(type) {
22
23
  const implementation = this.implementations.get(type.toLowerCase());
@@ -1,5 +1,6 @@
1
- import { PrinterImplementation } from "../types.js";
2
- export declare class PrusaImplementation extends PrinterImplementation {
1
+ import { GenericPrinterImplementation, type GenericPrinterState, type HeaterComponent } from "./generic-base.js";
2
+ export declare class PrusaImplementation extends GenericPrinterImplementation {
3
+ protected readonly printerLabel = "Prusa";
3
4
  private buildAuthHeaders;
4
5
  private buildBaseUrl;
5
6
  private defaultProtocolFor;
@@ -11,8 +12,15 @@ export declare class PrusaImplementation extends PrinterImplementation {
11
12
  getStatus(host: string, port: string, apiKey: string): Promise<any>;
12
13
  getFiles(host: string, port: string, apiKey: string): Promise<any>;
13
14
  getFile(host: string, port: string, apiKey: string, filename: string): Promise<any>;
14
- uploadFile(host: string, port: string, apiKey: string, filePath: string, filename: string, print: boolean): Promise<any>;
15
- startJob(host: string, port: string, apiKey: string, filename: string): Promise<any>;
16
- cancelJob(host: string, port: string, apiKey: string): Promise<any>;
17
- setTemperature(host: string, port: string, apiKey: string, component: string, temperature: number): Promise<any>;
15
+ /**
16
+ * PrusaLink /api/v1/status printer.state, falling back to the legacy
17
+ * OctoPrint-compatible /api/printer flags. FINISHED and STOPPED are idle but
18
+ * may leave a part on the bed, so they require a human confirmation.
19
+ */
20
+ protected readPrinterState(host: string, port: string, apiKey: string): Promise<GenericPrinterState>;
21
+ protected rawUploadFile(host: string, port: string, apiKey: string, filePath: string, filename: string, print: boolean): Promise<any>;
22
+ protected rawStartJob(host: string, port: string, apiKey: string, filename: string): Promise<any>;
23
+ protected rawCancelJob(host: string, port: string, apiKey: string): Promise<any>;
24
+ protected heaterComponent(component: string): HeaterComponent | undefined;
25
+ protected rawSetTemperature(host: string, port: string, apiKey: string, component: string, temperature: number): Promise<any>;
18
26
  }
@@ -1,8 +1,14 @@
1
1
  import { isAxiosError } from "axios";
2
- import { PrinterImplementation } from "../types.js";
2
+ import { GenericPrinterImplementation } from "./generic-base.js";
3
3
  import fs from "fs";
4
4
  import FormData from "form-data";
5
- export class PrusaImplementation extends PrinterImplementation {
5
+ const PRUSALINK_READY_STATES = new Set(["IDLE", "READY", "FINISHED", "STOPPED"]);
6
+ const OCTOPRINT_BLOCKING_FLAGS = ["printing", "paused", "pausing", "cancelling", "resuming", "finishing", "error", "closedOrError"];
7
+ export class PrusaImplementation extends GenericPrinterImplementation {
8
+ constructor() {
9
+ super(...arguments);
10
+ this.printerLabel = "Prusa";
11
+ }
6
12
  buildAuthHeaders(apiKey) {
7
13
  return {
8
14
  Accept: "application/json",
@@ -114,7 +120,41 @@ export class PrusaImplementation extends PrinterImplementation {
114
120
  ]);
115
121
  return response.data;
116
122
  }
117
- async uploadFile(host, port, apiKey, filePath, filename, print) {
123
+ /**
124
+ * PrusaLink /api/v1/status printer.state, falling back to the legacy
125
+ * OctoPrint-compatible /api/printer flags. FINISHED and STOPPED are idle but
126
+ * may leave a part on the bed, so they require a human confirmation.
127
+ */
128
+ async readPrinterState(host, port, apiKey) {
129
+ let response;
130
+ try {
131
+ response = await this.getWithFallback(host, port, apiKey, ["/api/v1/status", "/api/printer"]);
132
+ }
133
+ catch (error) {
134
+ throw new Error(`Cannot read Prusa printer state; nothing was sent: ${error.message}`);
135
+ }
136
+ const data = response.data;
137
+ const v1State = data?.printer?.state;
138
+ if (typeof v1State === "string" && v1State) {
139
+ const state = v1State.toUpperCase();
140
+ const ready = PRUSALINK_READY_STATES.has(state);
141
+ return {
142
+ state,
143
+ ready,
144
+ reason: ready ? undefined : `printer.state is ${state}`,
145
+ finishedJob: ready && (state === "FINISHED" || state === "STOPPED") ? JSON.stringify([state, data?.job?.id ?? null]) : undefined,
146
+ };
147
+ }
148
+ const flags = data?.state?.flags;
149
+ if (flags && typeof flags === "object") {
150
+ const blocking = OCTOPRINT_BLOCKING_FLAGS.filter((flag) => flags[flag] === true);
151
+ if (flags.operational !== true)
152
+ blocking.unshift("not operational");
153
+ return { state: String(data?.state?.text ?? "unknown"), ready: blocking.length === 0, reason: blocking.join(", ") || undefined };
154
+ }
155
+ throw new Error("Prusa did not report printer.state or state flags; cannot verify the printer is idle.");
156
+ }
157
+ async rawUploadFile(host, port, apiKey, filePath, filename, print) {
118
158
  const formData = new FormData();
119
159
  formData.append("file", fs.createReadStream(filePath));
120
160
  formData.append("filename", filename);
@@ -126,12 +166,10 @@ export class PrusaImplementation extends PrinterImplementation {
126
166
  ...formData.getHeaders(),
127
167
  },
128
168
  });
129
- if (print) {
130
- await this.startJob(host, port, apiKey, filename);
131
- }
169
+ // Printing is started by the shared gate only after a fresh state recheck.
132
170
  return response.data;
133
171
  }
134
- async startJob(host, port, apiKey, filename) {
172
+ async rawStartJob(host, port, apiKey, filename) {
135
173
  const response = await this.postWithFallback(host, port, apiKey, [
136
174
  {
137
175
  route: "/api/v1/job",
@@ -164,7 +202,7 @@ export class PrusaImplementation extends PrinterImplementation {
164
202
  ]);
165
203
  return response.data;
166
204
  }
167
- async cancelJob(host, port, apiKey) {
205
+ async rawCancelJob(host, port, apiKey) {
168
206
  const response = await this.postWithFallback(host, port, apiKey, [
169
207
  {
170
208
  route: "/api/v1/job",
@@ -181,7 +219,16 @@ export class PrusaImplementation extends PrinterImplementation {
181
219
  ]);
182
220
  return response.data;
183
221
  }
184
- async setTemperature(host, port, apiKey, component, temperature) {
222
+ heaterComponent(component) {
223
+ const normalized = component.trim().toLowerCase();
224
+ if (normalized === "bed")
225
+ return { heater: "bed", raw: "bed" };
226
+ if (normalized.startsWith("extruder") || normalized === "nozzle" || normalized === "tool0" || normalized === "tool") {
227
+ return { heater: "nozzle", raw: "nozzle" };
228
+ }
229
+ return undefined;
230
+ }
231
+ async rawSetTemperature(host, port, apiKey, component, temperature) {
185
232
  const normalized = component.toLowerCase();
186
233
  if (normalized === "bed") {
187
234
  const response = await this.postWithFallback(host, port, apiKey, [
@@ -1,10 +1,22 @@
1
- import { PrinterImplementation } from "../types.js";
2
- export declare class RepetierImplementation extends PrinterImplementation {
1
+ import { GenericPrinterImplementation, type GenericPrinterState } from "./generic-base.js";
2
+ /**
3
+ * Note: Repetier-Server documents printer commands at /printer/api/<slug>;
4
+ * this adapter's slug-less routes are unverified. Remote starts are refused
5
+ * because Repetier documents no complete file download for inspection.
6
+ */
7
+ export declare class RepetierImplementation extends GenericPrinterImplementation {
8
+ protected readonly printerLabel = "Repetier";
9
+ protected readonly uploadStartsPrint = true;
3
10
  getStatus(host: string, port: string, apiKey: string): Promise<any>;
4
11
  getFiles(host: string, port: string, apiKey: string): Promise<any>;
5
12
  getFile(host: string, port: string, apiKey: string, filename: string): Promise<any>;
6
- uploadFile(host: string, port: string, apiKey: string, filePath: string, filename: string, print: boolean): Promise<any>;
7
- startJob(host: string, port: string, apiKey: string, filename: string): Promise<any>;
8
- cancelJob(host: string, port: string, apiKey: string): Promise<any>;
9
- setTemperature(host: string, port: string, apiKey: string, component: string, temperature: number): Promise<any>;
13
+ /**
14
+ * listPrinter returns one entry per printer (online 0/1, active, job "none").
15
+ * Without a configured slug, more than one printer is ambiguous.
16
+ */
17
+ protected readPrinterState(host: string, port: string, apiKey: string): Promise<GenericPrinterState>;
18
+ protected rawUploadFile(host: string, port: string, apiKey: string, filePath: string, filename: string, print: boolean): Promise<any>;
19
+ protected rawStartJob(host: string, port: string, apiKey: string, filename: string): Promise<any>;
20
+ protected rawCancelJob(host: string, port: string, apiKey: string): Promise<any>;
21
+ protected rawSetTemperature(host: string, port: string, apiKey: string, component: string, temperature: number): Promise<any>;
10
22
  }
@@ -1,7 +1,18 @@
1
- import { PrinterImplementation } from "../types.js";
2
1
  import fs from "fs";
3
2
  import FormData from "form-data";
4
- export class RepetierImplementation extends PrinterImplementation {
3
+ import { GenericPrinterImplementation } from "./generic-base.js";
4
+ /**
5
+ * Note: Repetier-Server documents printer commands at /printer/api/<slug>;
6
+ * this adapter's slug-less routes are unverified. Remote starts are refused
7
+ * because Repetier documents no complete file download for inspection.
8
+ */
9
+ export class RepetierImplementation extends GenericPrinterImplementation {
10
+ constructor() {
11
+ super(...arguments);
12
+ this.printerLabel = "Repetier";
13
+ // The upload form's print=1 flag uploads and starts the same bytes.
14
+ this.uploadStartsPrint = true;
15
+ }
5
16
  async getStatus(host, port, apiKey) {
6
17
  const url = `http://${host}:${port}/printer/api/?a=getPrinterInfo&apikey=${apiKey}`;
7
18
  const response = await this.apiClient.get(url);
@@ -17,7 +28,38 @@ export class RepetierImplementation extends PrinterImplementation {
17
28
  const response = await this.apiClient.get(url);
18
29
  return response.data;
19
30
  }
20
- async uploadFile(host, port, apiKey, filePath, filename, print) {
31
+ /**
32
+ * listPrinter returns one entry per printer (online 0/1, active, job "none").
33
+ * Without a configured slug, more than one printer is ambiguous.
34
+ */
35
+ async readPrinterState(host, port, apiKey) {
36
+ let data;
37
+ try {
38
+ data = (await this.apiClient.get(`http://${host}:${port}/printer/api/?a=listPrinter&apikey=${encodeURIComponent(apiKey)}`)).data;
39
+ }
40
+ catch (error) {
41
+ throw new Error(`Cannot read Repetier printer state; nothing was sent: ${error.message}`);
42
+ }
43
+ const printers = Array.isArray(data) ? data : Array.isArray(data?.data) ? data.data : undefined;
44
+ if (!printers)
45
+ throw new Error("Repetier did not return a printer list; cannot verify the printer is idle.");
46
+ if (printers.length !== 1) {
47
+ throw new Error(`Repetier reports ${printers.length} printers; this adapter cannot tell which one would receive the job.`);
48
+ }
49
+ const [printer] = printers;
50
+ const online = printer?.online === 1 || printer?.online === true;
51
+ const job = typeof printer?.job === "string" ? printer.job : undefined;
52
+ if (job === undefined)
53
+ throw new Error("Repetier did not report the printer's job state; cannot verify it is idle.");
54
+ const reasons = [
55
+ ...(online ? [] : ["offline"]),
56
+ ...(printer?.active === false ? ["inactive"] : []),
57
+ ...(job !== "none" && job !== "" ? [`job ${job} is active`] : []),
58
+ ...(printer?.paused === true ? ["paused"] : []),
59
+ ];
60
+ return { state: `${online ? "online" : "offline"}, job ${job || "none"}`, ready: reasons.length === 0, reason: reasons.join(", ") || undefined };
61
+ }
62
+ async rawUploadFile(host, port, apiKey, filePath, filename, print) {
21
63
  const url = `http://${host}:${port}/printer/api/`;
22
64
  const formData = new FormData();
23
65
  formData.append("a", "upload");
@@ -32,17 +74,17 @@ export class RepetierImplementation extends PrinterImplementation {
32
74
  });
33
75
  return response.data;
34
76
  }
35
- async startJob(host, port, apiKey, filename) {
77
+ async rawStartJob(host, port, apiKey, filename) {
36
78
  const url = `http://${host}:${port}/printer/api/?a=startJob&apikey=${apiKey}&filename=${encodeURIComponent(filename)}`;
37
79
  const response = await this.apiClient.get(url);
38
80
  return response.data;
39
81
  }
40
- async cancelJob(host, port, apiKey) {
82
+ async rawCancelJob(host, port, apiKey) {
41
83
  const url = `http://${host}:${port}/printer/api/?a=stopJob&apikey=${apiKey}`;
42
84
  const response = await this.apiClient.get(url);
43
85
  return response.data;
44
86
  }
45
- async setTemperature(host, port, apiKey, component, temperature) {
87
+ async rawSetTemperature(host, port, apiKey, component, temperature) {
46
88
  let url;
47
89
  if (component === "bed") {
48
90
  url = `http://${host}:${port}/printer/api/?a=setBedTemp&apikey=${apiKey}&temp=${temperature}`;
@@ -0,0 +1,11 @@
1
+ import JSZip from 'jszip';
2
+ export declare const MAX_PRINT_FILE_BYTES: number;
3
+ /** Read no more than the fixed print-artifact limit, even if the file grows. */
4
+ export declare function readBoundedPrintFile(filePath: string): Promise<Buffer>;
5
+ /** Bounded native inflation prevents false size fields from making JSZip allocate
6
+ * unbounded output. Populate JSZip only with the verified, uniquely named bytes. */
7
+ export declare function loadSafe3mfArchive(bytes: Buffer): Promise<JSZip>;
8
+ export declare function readSafe3mfArchive(filePath: string): Promise<{
9
+ bytes: Buffer;
10
+ zip: JSZip;
11
+ }>;