bambu-printer-mcp 1.0.4 → 1.0.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.
@@ -0,0 +1,63 @@
1
+ type JsonObject = Record<string, unknown>;
2
+ export type BambuNetworkBridgeOptions = {
3
+ bridgeCommand?: string;
4
+ configDir?: string;
5
+ countryCode?: string;
6
+ userInfo?: string;
7
+ timeoutMs?: number;
8
+ };
9
+ export type BambuNetworkBridgeStatus = {
10
+ configured: boolean;
11
+ command: string;
12
+ platform: NodeJS.Platform;
13
+ running: boolean;
14
+ pid?: number;
15
+ configDir: string;
16
+ countryCode: string;
17
+ agentReady: boolean;
18
+ lastStderr: string;
19
+ recentLogs: string[];
20
+ runtime: {
21
+ macosRuntimeDir: string;
22
+ macosRuntimeDirExists: boolean;
23
+ macosPluginDir?: string;
24
+ macosPluginDirExists?: boolean;
25
+ macosMissingRuntimeFiles: string[];
26
+ macosMissingPluginFiles: string[];
27
+ suggestedMacCommand?: string;
28
+ };
29
+ note?: string;
30
+ };
31
+ export declare function defaultBambuNetworkConfigDir(): string;
32
+ export declare class BambuNetworkBridge {
33
+ private child?;
34
+ private commandLine?;
35
+ private stdoutBuffer;
36
+ private pending;
37
+ private nextRequestId;
38
+ private agentId?;
39
+ private agentKey?;
40
+ private handshake?;
41
+ private lastStderr;
42
+ private recentLogs;
43
+ private inferredExpectedNetworkVersion?;
44
+ resolveBridgeCommand(override?: string): string;
45
+ resolveConfigDir(override?: string): string;
46
+ resolveCountryCode(override?: string): string;
47
+ getStatus(options?: BambuNetworkBridgeOptions): BambuNetworkBridgeStatus;
48
+ request(method: string, payload?: JsonObject, options?: BambuNetworkBridgeOptions): Promise<unknown>;
49
+ ensureAgent(options?: BambuNetworkBridgeOptions): Promise<{
50
+ agent: number;
51
+ handshake: unknown;
52
+ }>;
53
+ callWithAgent(method: string, payload?: JsonObject, options?: BambuNetworkBridgeOptions): Promise<unknown>;
54
+ stop(): Promise<void>;
55
+ private ensureStarted;
56
+ private handleStdout;
57
+ private resolvePendingJson;
58
+ private rememberLog;
59
+ private rememberStderr;
60
+ private rejectAllPending;
61
+ private needsExpectedVersionRetry;
62
+ }
63
+ export {};
@@ -0,0 +1,393 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ const FRAME_MAGIC = 0x52424a50;
6
+ const FRAME_HEADER_SIZE = 16;
7
+ const FRAME_TYPE_JSON_REQUEST = 1;
8
+ const FRAME_TYPE_JSON_RESPONSE = 2;
9
+ const FRAME_TYPE_BINARY_DATA = 3;
10
+ const FRAME_TYPE_LOG = 4;
11
+ const DEFAULT_REQUEST_TIMEOUT_MS = 120000;
12
+ export function defaultBambuNetworkConfigDir() {
13
+ return path.join(os.homedir(), ".config", "bambu-printer-mcp", "bambu-network");
14
+ }
15
+ function shellQuote(value) {
16
+ return `'${value.replace(/'/g, "'\\''")}'`;
17
+ }
18
+ function trimEnv(value) {
19
+ const trimmed = value?.trim();
20
+ return trimmed ? trimmed : undefined;
21
+ }
22
+ function fileExists(filePath) {
23
+ try {
24
+ return fs.existsSync(filePath);
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ function resolveFirstExistingDirectory(candidates) {
31
+ return candidates.find((candidate) => fileExists(candidate) && fs.statSync(candidate).isDirectory());
32
+ }
33
+ function resolveFirstDirectoryWithFile(candidates, requiredFile) {
34
+ return candidates.find((candidate) => fileExists(candidate) &&
35
+ fs.statSync(candidate).isDirectory() &&
36
+ fileExists(path.join(candidate, requiredFile)));
37
+ }
38
+ function collectMissingFiles(baseDir, files) {
39
+ if (!baseDir)
40
+ return files;
41
+ return files.filter((file) => !fileExists(path.join(baseDir, file)));
42
+ }
43
+ function readFrameJson(payload) {
44
+ const text = payload.toString("utf8");
45
+ return text.length > 0 ? JSON.parse(text) : {};
46
+ }
47
+ function isBridgeFailure(value) {
48
+ return (typeof value === "object" &&
49
+ value !== null &&
50
+ value.ok === false);
51
+ }
52
+ function bridgeValueAsNumber(value, method) {
53
+ if (typeof value === "object" &&
54
+ value !== null &&
55
+ typeof value.value === "number") {
56
+ return value.value;
57
+ }
58
+ throw new Error(`FULU BambuNetwork bridge method ${method} did not return a numeric value.`);
59
+ }
60
+ function assertBridgeOk(value, method) {
61
+ if (isBridgeFailure(value)) {
62
+ const errorText = value.error === undefined ? "unknown bridge error" : String(value.error);
63
+ throw new Error(`FULU BambuNetwork bridge method ${method} failed: ${errorText}`);
64
+ }
65
+ }
66
+ export class BambuNetworkBridge {
67
+ constructor() {
68
+ this.stdoutBuffer = Buffer.alloc(0);
69
+ this.pending = new Map();
70
+ this.nextRequestId = 1;
71
+ this.lastStderr = "";
72
+ this.recentLogs = [];
73
+ }
74
+ resolveBridgeCommand(override) {
75
+ return (trimEnv(override) ||
76
+ trimEnv(process.env.BAMBU_NETWORK_BRIDGE_COMMAND) ||
77
+ trimEnv(process.env.FULU_BAMBU_NETWORK_BRIDGE_COMMAND) ||
78
+ trimEnv(process.env.PJARCZAK_BAMBU_BRIDGE_COMMAND) ||
79
+ "");
80
+ }
81
+ resolveConfigDir(override) {
82
+ return (trimEnv(override) ||
83
+ trimEnv(process.env.BAMBU_NETWORK_CONFIG_DIR) ||
84
+ trimEnv(process.env.PJARCZAK_BAMBU_PROBE_LOG_DIR) ||
85
+ defaultBambuNetworkConfigDir());
86
+ }
87
+ resolveCountryCode(override) {
88
+ return (trimEnv(override) ||
89
+ trimEnv(process.env.BAMBU_NETWORK_COUNTRY_CODE) ||
90
+ trimEnv(process.env.PJARCZAK_BAMBU_COUNTRY_CODE) ||
91
+ "US");
92
+ }
93
+ getStatus(options = {}) {
94
+ const command = this.resolveBridgeCommand(options.bridgeCommand);
95
+ const configDir = this.resolveConfigDir(options.configDir);
96
+ const countryCode = this.resolveCountryCode(options.countryCode);
97
+ const macosRuntimeDir = trimEnv(process.env.PJARCZAK_MAC_RUNTIME_DIR) ||
98
+ path.join(os.homedir(), "Library", "Application Support", "OrcaSlicer", "macos-bridge", "runtime");
99
+ const macosPluginDir = trimEnv(process.env.BAMBU_NETWORK_PLUGIN_DIR) ||
100
+ trimEnv(process.env.PJARCZAK_BAMBU_PLUGIN_DIR) ||
101
+ resolveFirstDirectoryWithFile([
102
+ path.join(os.homedir(), "Library", "Application Support", "OrcaSlicer", "plugins"),
103
+ "/Applications/Orca Studio.app/Contents/MacOS",
104
+ "/Applications/OrcaSlicer.app/Contents/MacOS",
105
+ "/Applications/OrcaSlicer-BMCU.app/Contents/MacOS",
106
+ "/Applications/Orca Studio.app/Contents/Resources",
107
+ "/Applications/OrcaSlicer.app/Contents/Resources",
108
+ "/Applications/OrcaSlicer-BMCU.app/Contents/Resources",
109
+ macosRuntimeDir,
110
+ ], "pjarczak-bambu-linux-host-wrapper") ||
111
+ resolveFirstExistingDirectory([macosRuntimeDir]);
112
+ const runtimeRequiredFiles = [
113
+ "libbambu_networking.so",
114
+ "libBambuSource.so",
115
+ "pjarczak_bambu_linux_host",
116
+ "pjarczak_bambu_linux_host_abi1",
117
+ "pjarczak_bambu_linux_host_abi0",
118
+ "ca-certificates.crt",
119
+ "slicer_base64.cer",
120
+ ];
121
+ const pluginRequiredFiles = [
122
+ "pjarczak-bambu-linux-host-wrapper",
123
+ "install_runtime_macos.sh",
124
+ "verify_runtime_macos.sh",
125
+ ];
126
+ let suggestedMacCommand;
127
+ if (macosPluginDir) {
128
+ const wrapper = path.join(macosPluginDir, "pjarczak-bambu-linux-host-wrapper");
129
+ const host = path.join(macosRuntimeDir, "pjarczak_bambu_linux_host");
130
+ suggestedMacCommand =
131
+ `PJARCZAK_BAMBU_PLUGIN_DIR=${shellQuote(macosRuntimeDir)} ` +
132
+ `${shellQuote(wrapper)} ${shellQuote(host)}`;
133
+ }
134
+ return {
135
+ configured: command.length > 0,
136
+ command,
137
+ platform: process.platform,
138
+ running: Boolean(this.child && this.child.exitCode === null),
139
+ pid: this.child?.pid,
140
+ configDir,
141
+ countryCode,
142
+ agentReady: this.agentId !== undefined,
143
+ lastStderr: this.lastStderr,
144
+ recentLogs: this.recentLogs.slice(-10),
145
+ runtime: {
146
+ macosRuntimeDir,
147
+ macosRuntimeDirExists: fileExists(macosRuntimeDir),
148
+ macosPluginDir,
149
+ macosPluginDirExists: macosPluginDir ? fileExists(macosPluginDir) : undefined,
150
+ macosMissingRuntimeFiles: collectMissingFiles(macosRuntimeDir, runtimeRequiredFiles),
151
+ macosMissingPluginFiles: collectMissingFiles(macosPluginDir, pluginRequiredFiles),
152
+ suggestedMacCommand,
153
+ },
154
+ note: command
155
+ ? undefined
156
+ : "Set BAMBU_NETWORK_BRIDGE_COMMAND to the FULU OrcaSlicer-bambulab Linux host or macOS/WSL wrapper command.",
157
+ };
158
+ }
159
+ async request(method, payload = {}, options = {}) {
160
+ if (!method.trim()) {
161
+ throw new Error("BambuNetwork bridge method is required.");
162
+ }
163
+ await this.ensureStarted(options.bridgeCommand);
164
+ const id = this.nextRequestId++;
165
+ const requestPayload = Buffer.from(JSON.stringify({ method, payload }), "utf8");
166
+ const frame = Buffer.alloc(FRAME_HEADER_SIZE + requestPayload.length);
167
+ frame.writeUInt32LE(FRAME_MAGIC, 0);
168
+ frame.writeUInt32LE(FRAME_TYPE_JSON_REQUEST, 4);
169
+ frame.writeUInt32LE(id, 8);
170
+ frame.writeUInt32LE(requestPayload.length, 12);
171
+ requestPayload.copy(frame, FRAME_HEADER_SIZE);
172
+ return await new Promise((resolve, reject) => {
173
+ const timer = setTimeout(() => {
174
+ this.pending.delete(id);
175
+ reject(new Error(`Timed out waiting for FULU BambuNetwork bridge method ${method}.`));
176
+ }, options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS);
177
+ this.pending.set(id, { method, resolve, reject, timer });
178
+ try {
179
+ this.child.stdin.write(frame, (error) => {
180
+ if (error) {
181
+ const pending = this.pending.get(id);
182
+ if (pending) {
183
+ clearTimeout(pending.timer);
184
+ this.pending.delete(id);
185
+ pending.reject(error);
186
+ }
187
+ }
188
+ });
189
+ }
190
+ catch (error) {
191
+ clearTimeout(timer);
192
+ this.pending.delete(id);
193
+ reject(error instanceof Error ? error : new Error(String(error)));
194
+ }
195
+ });
196
+ }
197
+ async ensureAgent(options = {}) {
198
+ const command = this.resolveBridgeCommand(options.bridgeCommand);
199
+ const configDir = this.resolveConfigDir(options.configDir);
200
+ const countryCode = this.resolveCountryCode(options.countryCode);
201
+ const userInfo = trimEnv(options.userInfo) || trimEnv(process.env.BAMBU_NETWORK_USER_INFO) || "";
202
+ const key = `${command}\n${configDir}\n${countryCode}\n${userInfo}`;
203
+ if (this.agentId !== undefined && this.agentKey === key) {
204
+ return { agent: this.agentId, handshake: this.handshake };
205
+ }
206
+ fs.mkdirSync(configDir, { recursive: true });
207
+ this.handshake = await this.request("bridge.handshake", {}, options);
208
+ assertBridgeOk(this.handshake, "bridge.handshake");
209
+ if (this.needsExpectedVersionRetry(this.handshake) && !trimEnv(process.env.PJARCZAK_EXPECTED_BAMBU_NETWORK_VERSION)) {
210
+ this.inferredExpectedNetworkVersion = this.handshake.network_actual_abi_version;
211
+ await this.stop();
212
+ this.handshake = await this.request("bridge.handshake", {}, options);
213
+ assertBridgeOk(this.handshake, "bridge.handshake");
214
+ }
215
+ const created = await this.request("net.create_agent", { log_dir: configDir, country_code: countryCode }, options);
216
+ assertBridgeOk(created, "net.create_agent");
217
+ const agent = bridgeValueAsNumber(created, "net.create_agent");
218
+ const withAgent = async (method, payload = {}, hardFail = true) => {
219
+ const response = await this.request(method, { ...payload, agent }, options);
220
+ if (hardFail)
221
+ assertBridgeOk(response, method);
222
+ return response;
223
+ };
224
+ await withAgent("net.set_config_dir", { config_dir: configDir });
225
+ await withAgent("net.init_log");
226
+ await withAgent("net.set_country_code", { country_code: countryCode });
227
+ await withAgent("net.start");
228
+ await withAgent("net.connect_server", {}, false);
229
+ if (userInfo) {
230
+ await withAgent("net.change_user", { user_info: userInfo });
231
+ }
232
+ this.agentId = agent;
233
+ this.agentKey = key;
234
+ return { agent, handshake: this.handshake };
235
+ }
236
+ async callWithAgent(method, payload = {}, options = {}) {
237
+ const { agent } = await this.ensureAgent(options);
238
+ return await this.request(method, { ...payload, agent }, options);
239
+ }
240
+ async stop() {
241
+ const child = this.child;
242
+ this.child = undefined;
243
+ this.commandLine = undefined;
244
+ this.agentId = undefined;
245
+ this.agentKey = undefined;
246
+ this.handshake = undefined;
247
+ this.stdoutBuffer = Buffer.alloc(0);
248
+ for (const [id, pending] of this.pending.entries()) {
249
+ clearTimeout(pending.timer);
250
+ pending.reject(new Error(`FULU BambuNetwork bridge stopped before ${pending.method} completed.`));
251
+ this.pending.delete(id);
252
+ }
253
+ if (child && child.exitCode === null) {
254
+ await new Promise((resolve) => {
255
+ const forceTimer = setTimeout(() => {
256
+ if (child.exitCode === null) {
257
+ child.kill("SIGKILL");
258
+ }
259
+ }, 2000);
260
+ child.once("close", () => {
261
+ clearTimeout(forceTimer);
262
+ resolve();
263
+ });
264
+ child.kill("SIGTERM");
265
+ });
266
+ }
267
+ }
268
+ async ensureStarted(commandOverride) {
269
+ const command = this.resolveBridgeCommand(commandOverride);
270
+ if (!command) {
271
+ throw new Error("BAMBU_NETWORK_BRIDGE_COMMAND is required for FULU BambuNetwork support. " +
272
+ "Point it at pjarczak_bambu_linux_host or the OrcaSlicer-bambulab macOS/WSL wrapper.");
273
+ }
274
+ if (this.child && this.child.exitCode === null && this.commandLine === command) {
275
+ return;
276
+ }
277
+ if (this.child && this.commandLine !== command) {
278
+ await this.stop();
279
+ }
280
+ const shell = process.platform === "win32" ? process.env.ComSpec || "cmd.exe" : "/bin/sh";
281
+ const shellArgs = process.platform === "win32"
282
+ ? ["/d", "/s", "/c", command]
283
+ : ["-lc", command];
284
+ this.child = spawn(shell, shellArgs, {
285
+ env: {
286
+ ...process.env,
287
+ PJARCZAK_BAMBU_COUNTRY_CODE: this.resolveCountryCode(),
288
+ PJARCZAK_EXPECTED_BAMBU_NETWORK_VERSION: this.inferredExpectedNetworkVersion ||
289
+ process.env.PJARCZAK_EXPECTED_BAMBU_NETWORK_VERSION ||
290
+ "",
291
+ },
292
+ stdio: ["pipe", "pipe", "pipe"],
293
+ });
294
+ this.commandLine = command;
295
+ const spawnedChild = this.child;
296
+ spawnedChild.stdout.on("data", (chunk) => this.handleStdout(chunk));
297
+ spawnedChild.stderr.on("data", (chunk) => this.rememberStderr(chunk.toString("utf8")));
298
+ spawnedChild.on("error", (error) => {
299
+ if (this.child === spawnedChild) {
300
+ this.rejectAllPending(error);
301
+ }
302
+ });
303
+ spawnedChild.on("close", (code, signal) => {
304
+ if (this.child !== spawnedChild) {
305
+ return;
306
+ }
307
+ this.rejectAllPending(new Error(`FULU BambuNetwork bridge exited (code=${code ?? "null"}, signal=${signal ?? "null"}). ${this.lastStderr}`.trim()));
308
+ this.child = undefined;
309
+ this.commandLine = undefined;
310
+ this.agentId = undefined;
311
+ this.agentKey = undefined;
312
+ this.handshake = undefined;
313
+ });
314
+ }
315
+ handleStdout(chunk) {
316
+ this.stdoutBuffer = Buffer.concat([this.stdoutBuffer, chunk]);
317
+ while (this.stdoutBuffer.length >= FRAME_HEADER_SIZE) {
318
+ const magic = this.stdoutBuffer.readUInt32LE(0);
319
+ if (magic !== FRAME_MAGIC) {
320
+ const preview = this.stdoutBuffer.subarray(0, Math.min(this.stdoutBuffer.length, 120)).toString("utf8");
321
+ this.rejectAllPending(new Error(`Invalid FULU BambuNetwork bridge frame magic. Output began with: ${preview}`));
322
+ this.stdoutBuffer = Buffer.alloc(0);
323
+ return;
324
+ }
325
+ const type = this.stdoutBuffer.readUInt32LE(4);
326
+ const id = this.stdoutBuffer.readUInt32LE(8);
327
+ const size = this.stdoutBuffer.readUInt32LE(12);
328
+ if (this.stdoutBuffer.length < FRAME_HEADER_SIZE + size) {
329
+ return;
330
+ }
331
+ const payload = this.stdoutBuffer.subarray(FRAME_HEADER_SIZE, FRAME_HEADER_SIZE + size);
332
+ this.stdoutBuffer = this.stdoutBuffer.subarray(FRAME_HEADER_SIZE + size);
333
+ if (type === FRAME_TYPE_JSON_RESPONSE) {
334
+ this.resolvePendingJson(id, payload);
335
+ }
336
+ else if (type === FRAME_TYPE_LOG) {
337
+ this.rememberLog(payload.toString("utf8"));
338
+ }
339
+ else if (type === FRAME_TYPE_BINARY_DATA) {
340
+ this.rememberLog(`received ${payload.length} bytes of bridge binary data for request ${id}`);
341
+ }
342
+ else {
343
+ this.rememberLog(`ignored bridge frame type ${type} for request ${id}`);
344
+ }
345
+ }
346
+ }
347
+ resolvePendingJson(id, payload) {
348
+ const pending = this.pending.get(id);
349
+ if (!pending) {
350
+ this.rememberLog(`received response for unknown bridge request ${id}`);
351
+ return;
352
+ }
353
+ clearTimeout(pending.timer);
354
+ this.pending.delete(id);
355
+ try {
356
+ pending.resolve(readFrameJson(payload));
357
+ }
358
+ catch (error) {
359
+ pending.reject(error instanceof Error ? error : new Error(String(error)));
360
+ }
361
+ }
362
+ rememberLog(message) {
363
+ const trimmed = message.trim();
364
+ if (!trimmed)
365
+ return;
366
+ this.recentLogs.push(trimmed);
367
+ if (this.recentLogs.length > 25) {
368
+ this.recentLogs = this.recentLogs.slice(-25);
369
+ }
370
+ }
371
+ rememberStderr(message) {
372
+ const combined = `${this.lastStderr}${message}`;
373
+ this.lastStderr = combined.length > 4000 ? combined.slice(-4000) : combined;
374
+ }
375
+ rejectAllPending(error) {
376
+ for (const [id, pending] of this.pending.entries()) {
377
+ clearTimeout(pending.timer);
378
+ pending.reject(error);
379
+ this.pending.delete(id);
380
+ }
381
+ }
382
+ needsExpectedVersionRetry(handshake) {
383
+ if (typeof handshake !== "object" || handshake === null) {
384
+ return false;
385
+ }
386
+ const value = handshake;
387
+ return (value.network_loaded === false &&
388
+ typeof value.network_actual_abi_version === "string" &&
389
+ value.network_actual_abi_version.length > 0 &&
390
+ typeof value.network_status === "string" &&
391
+ value.network_status.toLowerCase().includes("expected abi version"));
392
+ }
393
+ }