scad-gltf 0.3.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli-utils.js CHANGED
@@ -117,7 +117,33 @@ export function askQuestion(message) {
117
117
  });
118
118
  }
119
119
 
120
- export function generateScadPreviewUrl(scadCode) {
120
+ export function logAIThinking(reasoning, isVerbose = false) {
121
+ if (!reasoning) return;
122
+ const lines = reasoning.trim().split("\n");
123
+ if (lines.length === 0) return;
124
+
125
+ console.log(`\n🤔 AI Thinking:`);
126
+ if (isVerbose || lines.length <= 8) {
127
+ console.log(` | ${lines.join("\n | ")}`);
128
+ return;
129
+ }
130
+
131
+ // Hybrid view: initial intent + final conclusions
132
+ const head = lines.slice(0, 3);
133
+ const tail = lines.slice(-5);
134
+ const hiddenCount = lines.length - head.length - tail.length;
135
+
136
+ console.log(` | ${head.join("\n | ")}`);
137
+ console.log(
138
+ ` | ... [${hiddenCount} lines hidden, pass --verbose to view full thought process]`,
139
+ );
140
+ console.log(` | ${tail.join("\n | ")}`);
141
+ }
142
+
143
+ export function generateScadPreviewUrl(
144
+ scadCode,
145
+ baseUrl = "http://localhost:3000",
146
+ ) {
121
147
  try {
122
148
  const deflated = zlib.deflateRawSync(Buffer.from(scadCode, "utf-8"));
123
149
  const base64 = deflated
@@ -125,7 +151,7 @@ export function generateScadPreviewUrl(scadCode) {
125
151
  .replace(/\+/g, "-")
126
152
  .replace(/\//g, "_")
127
153
  .replace(/=+$/, "");
128
- return `https://iliagrigorevdev.github.io/scad-gltf/#c${base64}`;
154
+ return `${baseUrl}/#c${base64}`;
129
155
  } catch (e) {
130
156
  return null;
131
157
  }
@@ -139,11 +165,18 @@ export async function runAutomatedAIFlow(
139
165
  inputRequest,
140
166
  allowedTools = ["render_scad_model", "test_godot_project"],
141
167
  projectType = "godot",
168
+ flowOptions = {},
142
169
  ) {
143
170
  if (!baseUrl) {
144
171
  throw new Error("Base URL is required to run the automated AI flow.");
145
172
  }
146
173
 
174
+ const isVerbose = Boolean(
175
+ flowOptions.verbose ||
176
+ process.argv.includes("--verbose") ||
177
+ process.argv.includes("-v") ||
178
+ process.env.VERBOSE,
179
+ );
147
180
  const outputFilename = `generate_${projectType}_project.js`;
148
181
  const isRawScad = projectType === "scad";
149
182
  const modelTag = modelName ? ` (${modelName})` : "";
@@ -153,6 +186,18 @@ export async function runAutomatedAIFlow(
153
186
  let mcpClient = null;
154
187
  let mcpTransport = null;
155
188
  let openaiTools = [];
189
+ let serveProc = null;
190
+
191
+ if (isRawScad) {
192
+ console.log("🌐 Starting local viewer background server...");
193
+ const scadServePath = path.resolve(__dirname, "../bin/serve.js");
194
+ serveProc = spawn(process.execPath, [scadServePath], {
195
+ cwd: process.cwd(),
196
+ stdio: "ignore", // Hide Express logs from the terminal
197
+ detached: false,
198
+ });
199
+ await new Promise((resolve) => setTimeout(resolve, 1000));
200
+ }
156
201
 
157
202
  try {
158
203
  const mcpClientIndex =
@@ -208,439 +253,435 @@ export async function runAutomatedAIFlow(
208
253
  // Ensure the baseUrl points to the chat completions path
209
254
  const endpoint = baseUrl.replace(/\/+$/, "") + "/chat/completions";
210
255
 
211
- while (iterations < maxIterations) {
212
- iterations++;
213
- const waitingFrom = modelName ? ` from ${modelName}` : "";
214
- console.log(`\n🧠 Waiting for response${waitingFrom}...`);
256
+ try {
257
+ while (iterations < maxIterations) {
258
+ iterations++;
259
+ const waitingFrom = modelName ? ` from ${modelName}` : "";
260
+ console.log(`\n🧠 Waiting for response${waitingFrom}...`);
215
261
 
216
- const body = {
217
- messages: messages,
218
- };
262
+ const body = {
263
+ messages: messages,
264
+ };
219
265
 
220
- if (modelName) {
221
- body.model = modelName;
222
- }
266
+ if (modelName) {
267
+ body.model = modelName;
268
+ }
223
269
 
224
- if (openaiTools.length > 0) {
225
- body.tools = openaiTools;
226
- }
270
+ if (openaiTools.length > 0) {
271
+ body.tools = openaiTools;
272
+ }
227
273
 
228
- let response;
229
- try {
230
- response = await fetch(endpoint, {
231
- method: "POST",
232
- headers: {
233
- "Content-Type": "application/json",
234
- Authorization: `Bearer ${apiKey || "sk-no-key-required"}`,
235
- },
236
- body: JSON.stringify(body),
237
- });
238
- } catch (error) {
239
- throw new Error(`AI API Network Error: ${error.message}`);
240
- }
274
+ let response;
275
+ try {
276
+ response = await fetch(endpoint, {
277
+ method: "POST",
278
+ headers: {
279
+ "Content-Type": "application/json",
280
+ Authorization: `Bearer ${apiKey || "sk-no-key-required"}`,
281
+ },
282
+ body: JSON.stringify(body),
283
+ });
284
+ } catch (error) {
285
+ throw new Error(`AI API Network Error: ${error.message}`);
286
+ }
241
287
 
242
- if (!response.ok) {
243
- const errorText = await response.text();
244
-
245
- // Auto-fallback if the model complains about image/vision input
246
- const errLower = errorText.toLowerCase();
247
- if (
248
- !disableImageSupport &&
249
- (errLower.includes("image") ||
250
- errLower.includes("vision") ||
251
- errLower.includes("invalid_request"))
252
- ) {
253
- console.log(
254
- "\n⚠️ API rejected image input. Falling back to text-only mode and retrying...",
255
- );
256
- disableImageSupport = true;
257
-
258
- // Strip any existing images from previous messages
259
- for (const msg of messages) {
260
- if (Array.isArray(msg.content)) {
261
- msg.content = msg.content.filter((c) => c.type !== "image_url");
262
- // If only text parts remain, just keep them. If empty, add placeholder.
263
- if (msg.content.length === 0) {
264
- msg.content =
265
- "[Images removed by system fallback due to lack of model vision support]";
288
+ if (!response.ok) {
289
+ const errorText = await response.text();
290
+
291
+ // Auto-fallback if the model complains about image/vision input
292
+ const errLower = errorText.toLowerCase();
293
+ if (
294
+ !disableImageSupport &&
295
+ (errLower.includes("image") ||
296
+ errLower.includes("vision") ||
297
+ errLower.includes("invalid_request"))
298
+ ) {
299
+ console.log(
300
+ "\n⚠️ API rejected image input. Falling back to text-only mode and retrying...",
301
+ );
302
+ disableImageSupport = true;
303
+
304
+ // Strip any existing images from previous messages
305
+ for (const msg of messages) {
306
+ if (Array.isArray(msg.content)) {
307
+ msg.content = msg.content.filter((c) => c.type !== "image_url");
308
+ // If only text parts remain, just keep them. If empty, add placeholder.
309
+ if (msg.content.length === 0) {
310
+ msg.content =
311
+ "[Images removed by system fallback due to lack of model vision support]";
312
+ }
266
313
  }
267
314
  }
315
+
316
+ iterations--;
317
+ continue;
268
318
  }
269
319
 
270
- iterations--;
271
- continue;
320
+ throw new Error(
321
+ `AI API Error: ${response.status} ${response.statusText}\n${errorText}`,
322
+ );
272
323
  }
273
324
 
274
- throw new Error(
275
- `AI API Error: ${response.status} ${response.statusText}\n${errorText}`,
276
- );
277
- }
325
+ const data = await response.json();
326
+ const choice = data.choices[0];
327
+ const message = choice.message;
278
328
 
279
- const data = await response.json();
280
- const choice = data.choices[0];
281
- const message = choice.message;
329
+ const reasoning = message.reasoning_content || message.reasoning || "";
330
+ if (reasoning) {
331
+ logAIThinking(reasoning, isVerbose);
332
+ }
282
333
 
283
- // Append the assistant message exactly as provided (including tool_calls)
284
- messages.push(message);
334
+ // Append the assistant message exactly as provided (including tool_calls)
335
+ messages.push(message);
285
336
 
286
- if (message.tool_calls && message.tool_calls.length > 0) {
287
- const toolImagesUserContent = [];
337
+ if (message.tool_calls && message.tool_calls.length > 0) {
338
+ const toolImagesUserContent = [];
288
339
 
289
- for (const toolCall of message.tool_calls) {
290
- console.log(`\n⚙️ AI is using tool: ${toolCall.function.name}`);
340
+ for (const toolCall of message.tool_calls) {
341
+ console.log(`\n⚙️ AI is using tool: ${toolCall.function.name}`);
291
342
 
292
- let argsToLog = {};
293
- try {
294
- argsToLog = JSON.parse(toolCall.function.arguments);
295
- } catch (e) {
296
- console.error(
297
- " ❌ Failed to parse tool arguments:",
298
- toolCall.function.arguments,
343
+ let argsToLog = {};
344
+ try {
345
+ argsToLog = JSON.parse(toolCall.function.arguments);
346
+ } catch (e) {
347
+ console.error(
348
+ " ❌ Failed to parse tool arguments:",
349
+ toolCall.function.arguments,
350
+ );
351
+ }
352
+
353
+ // Log truncated arguments to avoid flooding the terminal
354
+ for (const key in argsToLog) {
355
+ if (
356
+ typeof argsToLog[key] === "string" &&
357
+ argsToLog[key].length > 300
358
+ ) {
359
+ argsToLog[key] =
360
+ argsToLog[key].substring(0, 300) +
361
+ "\n... [truncated for logging]";
362
+ }
363
+ }
364
+ console.log(
365
+ ` Args:`,
366
+ JSON.stringify(argsToLog, null, 2).replace(/\n/g, "\n "),
299
367
  );
300
- }
301
368
 
302
- // Log truncated arguments to avoid flooding the terminal
303
- for (const key in argsToLog) {
304
- if (
305
- typeof argsToLog[key] === "string" &&
306
- argsToLog[key].length > 300
307
- ) {
308
- argsToLog[key] =
309
- argsToLog[key].substring(0, 300) +
310
- "\n... [truncated for logging]";
369
+ if (!mcpClient) {
370
+ console.error(" ❌ MCP client not available.");
371
+ messages.push({
372
+ role: "tool",
373
+ tool_call_id: toolCall.id,
374
+ content: "MCP client not available.",
375
+ });
376
+ continue;
311
377
  }
312
- }
313
- console.log(
314
- ` Args:`,
315
- JSON.stringify(argsToLog, null, 2).replace(/\n/g, "\n "),
316
- );
317
378
 
318
- if (!mcpClient) {
319
- console.error(" ❌ MCP client not available.");
320
- messages.push({
321
- role: "tool",
322
- tool_call_id: toolCall.id,
323
- content: "MCP client not available.",
324
- });
325
- continue;
326
- }
379
+ try {
380
+ const callArgs = JSON.parse(toolCall.function.arguments);
327
381
 
328
- try {
329
- const callArgs = JSON.parse(toolCall.function.arguments);
382
+ if (disableImageSupport) {
383
+ callArgs.return_images = false;
384
+ }
330
385
 
331
- if (disableImageSupport) {
332
- callArgs.return_images = false;
333
- }
386
+ if (
387
+ toolCall.function.name === "render_scad_model" &&
388
+ callArgs.scad_code
389
+ ) {
390
+ const link = generateScadPreviewUrl(callArgs.scad_code);
391
+ if (link) {
392
+ console.log(
393
+ ` 🔗 Intermediate Web Preview Link:\n ${link}`,
394
+ );
395
+ }
396
+ }
334
397
 
335
- if (
336
- toolCall.function.name === "render_scad_model" &&
337
- callArgs.scad_code
338
- ) {
339
- const link = generateScadPreviewUrl(callArgs.scad_code);
340
- if (link) {
341
- console.log(
342
- ` 🔗 Intermediate Web Preview Link:\n ${link}`,
343
- );
398
+ const result = await mcpClient.callTool({
399
+ name: toolCall.function.name,
400
+ arguments: callArgs,
401
+ });
402
+
403
+ let responseText = "";
404
+ let imageContents = [];
405
+ for (const item of result.content) {
406
+ if (item.type === "text") responseText += item.text + "\n";
407
+ if (item.type === "image") {
408
+ imageContents.push({
409
+ type: "image_url",
410
+ image_url: {
411
+ url: `data:${item.mimeType};base64,${item.data}`,
412
+ },
413
+ });
414
+ }
344
415
  }
345
- }
346
416
 
347
- const result = await mcpClient.callTool({
348
- name: toolCall.function.name,
349
- arguments: callArgs,
350
- });
417
+ messages.push({
418
+ role: "tool",
419
+ tool_call_id: toolCall.id,
420
+ content: responseText || "Success",
421
+ });
351
422
 
352
- let responseText = "";
353
- let imageContents = [];
354
- for (const item of result.content) {
355
- if (item.type === "text") responseText += item.text + "\n";
356
- if (item.type === "image") {
357
- imageContents.push({
358
- type: "image_url",
359
- image_url: {
360
- url: `data:${item.mimeType};base64,${item.data}`,
361
- },
423
+ if (imageContents.length > 0) {
424
+ toolImagesUserContent.push({
425
+ type: "text",
426
+ text: `Visual result of ${toolCall.function.name}:`,
362
427
  });
428
+ toolImagesUserContent.push(...imageContents);
363
429
  }
364
- }
365
430
 
366
- messages.push({
367
- role: "tool",
368
- tool_call_id: toolCall.id,
369
- content: responseText || "Success",
370
- });
431
+ console.log(`✔️ Tool ${toolCall.function.name} completed.`);
371
432
 
372
- if (imageContents.length > 0) {
373
- toolImagesUserContent.push({
374
- type: "text",
375
- text: `Visual result of ${toolCall.function.name}:`,
433
+ if (responseText) {
434
+ const lines = responseText.trim().split("\n");
435
+ const previewLines = lines.slice(0, 15);
436
+ console.log(` Response:\n | ${previewLines.join("\n | ")}`);
437
+ if (lines.length > 15)
438
+ console.log(` | ... [${lines.length - 15} more lines]`);
439
+ }
440
+ } catch (err) {
441
+ console.error(`⚠️ Tool error: ${err.message}`);
442
+ messages.push({
443
+ role: "tool",
444
+ tool_call_id: toolCall.id,
445
+ content: `Error: ${err.message}`,
376
446
  });
377
- toolImagesUserContent.push(...imageContents);
378
447
  }
448
+ }
379
449
 
380
- console.log(`✔️ Tool ${toolCall.function.name} completed.`);
381
-
382
- if (responseText) {
383
- const lines = responseText.trim().split("\n");
384
- const previewLines = lines.slice(0, 15);
385
- console.log(` Response:\n | ${previewLines.join("\n | ")}`);
386
- if (lines.length > 15)
387
- console.log(` | ... [${lines.length - 15} more lines]`);
388
- }
389
- } catch (err) {
390
- console.error(`⚠️ Tool error: ${err.message}`);
450
+ // Append accumulated tool images as a follow-up user message so the AI can "see" the visual outputs
451
+ if (toolImagesUserContent.length > 0) {
391
452
  messages.push({
392
- role: "tool",
393
- tool_call_id: toolCall.id,
394
- content: `Error: ${err.message}`,
453
+ role: "user",
454
+ content: toolImagesUserContent,
395
455
  });
396
- }
397
- }
398
-
399
- // Append accumulated tool images as a follow-up user message so the AI can "see" the visual outputs
400
- if (toolImagesUserContent.length > 0) {
401
- messages.push({
402
- role: "user",
403
- content: toolImagesUserContent,
404
- });
405
- const numImages = toolImagesUserContent.filter(
406
- (c) => c.type === "image_url",
407
- ).length;
408
- console.log(` 🖼️ Appended ${numImages} image(s) to AI context.`);
409
- }
410
- } else {
411
- const finalText = message.content || "";
412
- console.log("\n✅ AI proposed a final code version.");
413
-
414
- let finalCode = "";
415
- let extractedFilename = outputFilename;
416
-
417
- // Extract the code payload based on the project type target
418
- if (isRawScad) {
419
- const match = finalText.match(/```(?:openscad|scad)?\n([\s\S]*?)```/i);
420
- finalCode = match ? match[1].trim() : finalText.trim();
421
-
422
- extractedFilename = "generated_model.scad";
423
- const nameMatch = finalCode.match(
424
- /\/\*\s*Model Name:\s*([^*]+)\s*\*\//i,
425
- );
426
- if (nameMatch) {
427
- let safeName = nameMatch[1]
428
- .trim()
429
- .toLowerCase()
430
- .replace(/[^a-z0-9_]/g, "_");
431
- if (!safeName.endsWith(".scad")) safeName += ".scad";
432
- extractedFilename = safeName;
456
+ const numImages = toolImagesUserContent.filter(
457
+ (c) => c.type === "image_url",
458
+ ).length;
459
+ console.log(` 🖼️ Appended ${numImages} image(s) to AI context.`);
433
460
  }
434
461
  } else {
435
- const match = finalText.match(
436
- /```(?:javascript|js|node)?\n([\s\S]*?)```/i,
437
- );
438
- finalCode = match ? match[1].trim() : finalText.trim();
439
- }
462
+ const finalText = message.content || "";
463
+ console.log("\n✅ AI proposed a final code version.");
440
464
 
441
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "scad-preview-"));
442
- const tempFilePath = path.join(tempDir, extractedFilename);
443
- let runProc = null;
444
-
445
- try {
446
- fs.writeFileSync(tempFilePath, finalCode, "utf-8");
465
+ let finalCode = "";
466
+ let extractedFilename = outputFilename;
447
467
 
468
+ // Extract the code payload based on the project type target
448
469
  if (isRawScad) {
449
- console.log(
450
- ` Generating OpenSCAD file to preview in a temporary folder...`,
470
+ const match = finalText.match(
471
+ /```(?:openscad|scad)?\n([\s\S]*?)```/i,
451
472
  );
452
- console.log(` Temp file: ${tempFilePath}`);
473
+ finalCode = match ? match[1].trim() : finalText.trim();
453
474
 
454
- const link = generateScadPreviewUrl(finalCode);
455
- if (link) {
456
- console.log(`\n🔗 Direct Web Preview Link:\n ${link}\n`);
475
+ extractedFilename = "generated_model.scad";
476
+ const nameMatch = finalCode.match(
477
+ /\/\*\s*Model Name:\s*([^*]+)\s*\*\//i,
478
+ );
479
+ if (nameMatch) {
480
+ let safeName = nameMatch[1]
481
+ .trim()
482
+ .toLowerCase()
483
+ .replace(/[^a-z0-9_]/g, "_");
484
+ if (!safeName.endsWith(".scad")) safeName += ".scad";
485
+ extractedFilename = safeName;
457
486
  }
458
-
459
- console.log(
460
- `\n🌐 Starting local viewer to preview ${extractedFilename}...`,
487
+ } else {
488
+ const match = finalText.match(
489
+ /```(?:javascript|js|node)?\n([\s\S]*?)```/i,
461
490
  );
491
+ finalCode = match ? match[1].trim() : finalText.trim();
492
+ }
462
493
 
463
- const scadServePath = path.resolve(__dirname, "../bin/serve.js");
464
- runProc = spawn(process.execPath, [scadServePath], {
465
- cwd: tempDir,
466
- stdio: "inherit",
467
- detached: false,
468
- });
494
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "scad-preview-"));
495
+ const tempFilePath = path.join(tempDir, extractedFilename);
496
+ let runProc = null;
469
497
 
470
- await new Promise((resolve) => setTimeout(resolve, 2000));
471
- } else {
472
- console.log(
473
- ` Generating project files to preview in a temporary folder...`,
474
- );
475
- console.log(` Temp script: ${tempFilePath}`);
498
+ try {
499
+ fs.writeFileSync(tempFilePath, finalCode, "utf-8");
500
+ if (isRawScad) {
501
+ console.log(
502
+ ` Generating OpenSCAD file to preview in a temporary folder...`,
503
+ );
504
+ console.log(` Temp file: ${tempFilePath}`);
476
505
 
477
- execSync(`node "${tempFilePath}"`, {
478
- cwd: tempDir,
479
- stdio: "pipe",
480
- });
506
+ const link = generateScadPreviewUrl(finalCode);
507
+ if (link) {
508
+ console.log(`\n🔗 Direct Web Preview Link:\n ${link}\n`);
509
+ }
510
+ } else {
511
+ console.log(
512
+ ` Generating project files to preview in a temporary folder...`,
513
+ );
514
+ console.log(` Temp script: ${tempFilePath}`);
515
+
516
+ execSync(`node "${tempFilePath}"`, {
517
+ cwd: tempDir,
518
+ stdio: "pipe",
519
+ });
481
520
 
482
- let projectDir = null;
483
- let latestTime = 0;
484
- const items = fs.readdirSync(tempDir);
485
-
486
- for (const item of items) {
487
- const itemPath = path.join(tempDir, item);
488
- if (fs.statSync(itemPath).isDirectory()) {
489
- const isGodot =
490
- projectType === "godot" &&
491
- fs.existsSync(path.join(itemPath, "project.godot"));
492
- const isWeb =
493
- projectType === "web" &&
494
- fs.existsSync(path.join(itemPath, "package.json"));
495
- const isBevy =
496
- projectType === "bevy" &&
497
- fs.existsSync(path.join(itemPath, "Cargo.toml"));
498
- const isWgpu =
499
- projectType === "wgpu" &&
500
- fs.existsSync(path.join(itemPath, "Cargo.toml"));
501
-
502
- if (isGodot || isWeb || isBevy || isWgpu) {
503
- const mtime = fs.statSync(itemPath).mtimeMs;
504
- if (mtime > latestTime) {
505
- latestTime = mtime;
506
- projectDir = itemPath;
521
+ let projectDir = null;
522
+ let latestTime = 0;
523
+ const items = fs.readdirSync(tempDir);
524
+
525
+ for (const item of items) {
526
+ const itemPath = path.join(tempDir, item);
527
+ if (fs.statSync(itemPath).isDirectory()) {
528
+ const isGodot =
529
+ projectType === "godot" &&
530
+ fs.existsSync(path.join(itemPath, "project.godot"));
531
+ const isWeb =
532
+ projectType === "web" &&
533
+ fs.existsSync(path.join(itemPath, "package.json"));
534
+ const isBevy =
535
+ projectType === "bevy" &&
536
+ fs.existsSync(path.join(itemPath, "Cargo.toml"));
537
+ const isWgpu =
538
+ projectType === "wgpu" &&
539
+ fs.existsSync(path.join(itemPath, "Cargo.toml"));
540
+
541
+ if (isGodot || isWeb || isBevy || isWgpu) {
542
+ const mtime = fs.statSync(itemPath).mtimeMs;
543
+ if (mtime > latestTime) {
544
+ latestTime = mtime;
545
+ projectDir = itemPath;
546
+ }
507
547
  }
508
548
  }
509
549
  }
510
- }
511
550
 
512
- if (!projectDir) {
513
- console.log(` ❌ Could not find the generated project folder.`);
514
- const expectedFile =
515
- projectType === "godot"
516
- ? "project.godot"
517
- : projectType === "bevy" || projectType === "wgpu"
518
- ? "Cargo.toml"
519
- : "package.json";
520
- messages.push({
521
- role: "user",
522
- content: `Your script successfully executed, but no valid project directory was found. Make sure your script creates a root folder and generates the correct required files (like ${expectedFile}) inside it.`,
523
- });
524
- continue;
525
- }
551
+ if (!projectDir) {
552
+ console.log(` ❌ Could not find the generated project folder.`);
553
+ const expectedFile =
554
+ projectType === "godot"
555
+ ? "project.godot"
556
+ : projectType === "bevy" || projectType === "wgpu"
557
+ ? "Cargo.toml"
558
+ : "package.json";
559
+ messages.push({
560
+ role: "user",
561
+ content: `Your script successfully executed, but no valid project directory was found. Make sure your script creates a root folder and generates the correct required files (like ${expectedFile}) inside it.`,
562
+ });
563
+ continue;
564
+ }
526
565
 
527
- console.log(` Project folder: ${projectDir}`);
566
+ console.log(` Project folder: ${projectDir}`);
528
567
 
529
- if (projectType === "godot") {
530
- console.log(
531
- `\n🎮 Launching project visually: ${path.basename(projectDir)}`,
532
- );
533
- const godotBin = getGodotBin();
534
- runProc = spawn(godotBin, ["--path", projectDir], {
535
- stdio: "ignore",
536
- detached: true,
537
- });
538
- runProc.unref();
539
- } else if (projectType === "web") {
540
- console.log(
541
- `\n🌐 Installing dependencies for: ${path.basename(projectDir)}...`,
542
- );
543
- try {
544
- execSync("npm install", { cwd: projectDir, stdio: "inherit" });
545
- } catch (e) {
568
+ if (projectType === "godot") {
546
569
  console.log(
547
- "⚠️ npm install failed, trying to continue anyway...",
570
+ `\n🎮 Launching project visually: ${path.basename(projectDir)}`,
548
571
  );
549
- }
572
+ const godotBin = getGodotBin();
573
+ runProc = spawn(godotBin, ["--path", projectDir], {
574
+ stdio: "ignore",
575
+ detached: true,
576
+ });
577
+ runProc.unref();
578
+ } else if (projectType === "web") {
579
+ console.log(
580
+ `\n🌐 Installing dependencies for: ${path.basename(projectDir)}...`,
581
+ );
582
+ try {
583
+ execSync("npm install", { cwd: projectDir, stdio: "inherit" });
584
+ } catch (e) {
585
+ console.log(
586
+ "⚠️ npm install failed, trying to continue anyway...",
587
+ );
588
+ }
550
589
 
551
- console.log(`\n🌐 Starting dev server...`);
552
- const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
553
- runProc = spawn(npmCmd, ["run", "dev"], {
554
- cwd: projectDir,
555
- stdio: "inherit",
556
- detached: false,
557
- });
590
+ console.log(`\n🌐 Starting dev server...`);
591
+ const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
592
+ runProc = spawn(npmCmd, ["run", "dev"], {
593
+ cwd: projectDir,
594
+ stdio: "inherit",
595
+ detached: false,
596
+ });
558
597
 
559
- await new Promise((resolve) => setTimeout(resolve, 2000));
560
- } else if (projectType === "bevy" || projectType === "wgpu") {
561
- const fwName = projectType === "bevy" ? "Bevy" : "wgpu";
562
- console.log(
563
- `\n🦀 Building and running ${fwName} project: ${path.basename(projectDir)}...`,
564
- );
565
- const cargoCmd =
566
- process.platform === "win32" ? "cargo.exe" : "cargo";
567
- runProc = spawn(cargoCmd, ["run"], {
568
- cwd: projectDir,
569
- stdio: "inherit",
570
- detached: false,
571
- });
572
- await new Promise((resolve) => setTimeout(resolve, 2000));
598
+ await new Promise((resolve) => setTimeout(resolve, 2000));
599
+ } else if (projectType === "bevy" || projectType === "wgpu") {
600
+ const fwName = projectType === "bevy" ? "Bevy" : "wgpu";
601
+ console.log(
602
+ `\n🦀 Building and running ${fwName} project: ${path.basename(projectDir)}...`,
603
+ );
604
+ const cargoCmd =
605
+ process.platform === "win32" ? "cargo.exe" : "cargo";
606
+ runProc = spawn(cargoCmd, ["run"], {
607
+ cwd: projectDir,
608
+ stdio: "inherit",
609
+ detached: false,
610
+ });
611
+ await new Promise((resolve) => setTimeout(resolve, 2000));
612
+ }
573
613
  }
574
- }
575
-
576
- const feedback = await askQuestion(
577
- "\nAre you happy with this result? (Type 'y' to accept, or type feedback for the AI to fix): ",
578
- );
579
614
 
580
- if (
581
- feedback.toLowerCase() === "y" ||
582
- feedback.toLowerCase() === "yes" ||
583
- feedback === ""
584
- ) {
585
- fs.copyFileSync(
586
- tempFilePath,
587
- path.resolve(process.cwd(), extractedFilename),
615
+ const feedback = await askQuestion(
616
+ "\nAre you happy with this result? (Type 'y' to accept, or type feedback for the AI to fix): ",
588
617
  );
589
618
 
590
- console.log(
591
- `\n🎉 Success! Final file finalized and written to: ${extractedFilename}`,
592
- );
619
+ if (
620
+ feedback.toLowerCase() === "y" ||
621
+ feedback.toLowerCase() === "yes" ||
622
+ feedback === ""
623
+ ) {
624
+ fs.copyFileSync(
625
+ tempFilePath,
626
+ path.resolve(process.cwd(), extractedFilename),
627
+ );
593
628
 
594
- if (!isRawScad) {
595
629
  console.log(
596
- `▶️ Run it again later to unpack the final project with: node ${extractedFilename}`,
630
+ `\n🎉 Success! Final file finalized and written to: ${extractedFilename}`,
597
631
  );
598
- }
599
632
 
600
- if (mcpTransport) {
601
- try {
602
- await mcpTransport.close();
603
- } catch (e) {}
633
+ if (!isRawScad) {
634
+ console.log(
635
+ `▶️ Run it again later to unpack the final project with: node ${extractedFilename}`,
636
+ );
637
+ }
638
+
639
+ break;
640
+ } else {
641
+ console.log("\n🔄 Sending your feedback back to the LLM...");
642
+ messages.push({
643
+ role: "user",
644
+ content: `I reviewed the current version. Here is my feedback to improve it:\n\n${feedback}\n\nPlease implement these fixes and output an updated ${isRawScad ? "OpenSCAD block" : "Node.js script"}.`,
645
+ });
604
646
  }
605
- break;
606
- } else {
607
- console.log("\n🔄 Sending your feedback back to the LLM...");
647
+ } catch (err) {
648
+ const stderr = err.stderr ? err.stderr.toString() : err.message;
649
+ console.error(
650
+ ` ❌ Failed to execute/preview generated code:\n${stderr}`,
651
+ );
608
652
  messages.push({
609
653
  role: "user",
610
- content: `I reviewed the current version. Here is my feedback to improve it:\n\n${feedback}\n\nPlease implement these fixes and output an updated ${isRawScad ? "OpenSCAD block" : "Node.js script"}.`,
654
+ content: `Your generated code threw an error when I tried to run/preview it locally:\n${stderr}\n\nPlease fix the ${isRawScad ? "OpenSCAD code" : "Node.js script"}.`,
611
655
  });
612
- }
613
- } catch (err) {
614
- const stderr = err.stderr ? err.stderr.toString() : err.message;
615
- console.error(
616
- ` ❌ Failed to execute/preview generated code:\n${stderr}`,
617
- );
618
- messages.push({
619
- role: "user",
620
- content: `Your generated code threw an error when I tried to run/preview it locally:\n${stderr}\n\nPlease fix the ${isRawScad ? "OpenSCAD code" : "Node.js script"}.`,
621
- });
622
- } finally {
623
- if (runProc) {
656
+ } finally {
657
+ if (runProc) {
658
+ try {
659
+ runProc.kill();
660
+ } catch (e) {}
661
+ }
624
662
  try {
625
- runProc.kill();
663
+ fs.rmSync(tempDir, { recursive: true, force: true });
626
664
  } catch (e) {}
627
665
  }
628
- try {
629
- fs.rmSync(tempDir, { recursive: true, force: true });
630
- } catch (e) {}
631
666
  }
632
667
  }
633
- }
634
668
 
635
- if (iterations >= maxIterations) {
636
- console.log(
637
- `\n⚠️ Reached maximum iterations (${maxIterations}). Terminating.`,
638
- );
669
+ if (iterations >= maxIterations) {
670
+ console.log(
671
+ `\n⚠️ Reached maximum iterations (${maxIterations}). Terminating.`,
672
+ );
673
+ }
674
+ } finally {
639
675
  if (mcpTransport) {
640
676
  try {
641
677
  await mcpTransport.close();
642
678
  } catch (e) {}
643
679
  }
680
+ if (serveProc) {
681
+ try {
682
+ serveProc.kill();
683
+ } catch (e) {}
684
+ }
644
685
  }
645
686
  }
646
687
 
@@ -670,11 +711,11 @@ export async function runCliApp({
670
711
  console.error("Examples with JSON options (Automated AI flow):");
671
712
  if (isRawScad) {
672
713
  console.error(
673
- ` scad-gltf gen ${projectType} "A modular sci-fi corridor piece" '{"openaiApiKey": "sk-...", "openaiModel": "gpt-4o"}'`,
714
+ ` scad-gltf gen ${projectType} "A modular sci-fi corridor piece" '{"openaiApiKey": "sk-...", "openaiModel": "gpt-4o", "scadFiles": ["./base.scad"]}'`,
674
715
  );
675
716
  } else {
676
717
  console.error(
677
- ` scad-gltf gen ${projectType} "A simple 3D simulation" '{"openaiApiKey": "sk-...", "openaiModel": "gpt-4o"}'`,
718
+ ` scad-gltf gen ${projectType} "A simple 3D simulation" '{"openaiApiKey": "sk-...", "openaiModel": "gpt-4o", "scadFiles": ["./character.scad"]}'`,
678
719
  );
679
720
  }
680
721
  process.exit(1);
@@ -736,13 +777,14 @@ export async function runCliApp({
736
777
  inputRequestOutput,
737
778
  allowedTools,
738
779
  projectType,
780
+ options,
739
781
  );
740
782
  return;
741
783
  }
742
784
 
743
785
  if (isRawScad) {
744
786
  try {
745
- await writeToClipboard(inputRequestOutput);
787
+ await writeToClipboard(systemClipboardOutput);
746
788
  console.log(
747
789
  "✔️ Input request and syntax rules have been copied to the clipboard. You can now paste it into your LLM.",
748
790
  );
@@ -754,31 +796,19 @@ export async function runCliApp({
754
796
  return;
755
797
  }
756
798
 
757
- // Write to System Clipboard (Part 1: System Instructions)
758
- try {
759
- await writeToClipboard(systemClipboardOutput);
760
- console.log("✔️ System instructions have been copied to the clipboard.");
761
- } catch (err) {
762
- console.error(
763
- "Error: Failed to copy system instructions to the clipboard.",
764
- );
765
- console.error(err.message);
766
- process.exit(1);
767
- }
799
+ const combinedOutput = `=== SYSTEM PROMPT ===
800
+ ${systemClipboardOutput}
768
801
 
769
- // Await user confirmation
770
- await waitForEnter(
771
- "Please paste the system instructions into your LLM, then press ENTER to copy your input request...",
772
- );
802
+ === USER REQUEST ===
803
+ ${inputRequestOutput}`;
773
804
 
774
- // Write to System Clipboard (Part 2: Input Request)
775
805
  try {
776
- await writeToClipboard(inputRequestOutput);
806
+ await writeToClipboard(combinedOutput);
777
807
  console.log(
778
- "✔️ Input request has been copied to the clipboard. You can now paste it into your LLM.",
808
+ "✔️ The system prompt and input request have been combined and copied to the clipboard. You can now paste them into your LLM.",
779
809
  );
780
810
  } catch (err) {
781
- console.error("Error: Failed to copy input request to the clipboard.");
811
+ console.error("Error: Failed to copy to the clipboard.");
782
812
  console.error(err.message);
783
813
  process.exit(1);
784
814
  }