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/README.md +46 -18
- package/bin/gen.js +1 -1
- package/bin/mcp.js +198 -69
- package/editor/dist/assets/{index-B1KAb97r.css → index-D8In1wnN.css} +1 -1
- package/editor/dist/assets/{index-CgVih1C4.js → index-b4iPmEjR.js} +204 -92
- package/editor/dist/assets/{openscad-CdBCY4mx.wasm → openscad-DJJQxKJM.wasm} +0 -0
- package/editor/dist/assets/preview-Cx3FHqKG.css +1 -0
- package/editor/dist/assets/preview-DfaOpP2w.js +1 -0
- package/editor/dist/assets/{prompt-ui-B9QCNLfJ.js → prompt-ui-zw7U8lsl.js} +5 -3
- package/editor/dist/assets/shared-CDFEFgsW.js +4315 -0
- package/editor/dist/content.js +1 -1
- package/editor/dist/index.html +11 -6
- package/editor/dist/preview.html +20 -15
- package/editor/dist/sw.js +1 -1
- package/godot/addons/scad_importer/plugin.cfg +0 -1
- package/godot/addons/scad_importer/scad_importer.gd +3 -153
- package/godot/addons/scad_importer/scad_plugin.gd +0 -3
- package/package.json +4 -4
- package/src/cli-utils.js +406 -376
- package/src/ext/openscad.js +1 -1
- package/src/ext/openscad.wasm +0 -0
- package/src/project-prompts.js +111 -87
- package/src/prompt.js +21 -0
- package/editor/dist/assets/OutputPass-B6MrFnGE.js +0 -4317
- package/editor/dist/assets/preview-A6M04CFK.css +0 -1
- package/editor/dist/assets/preview-BbUpKLF4.js +0 -1
- package/godot/README.md +0 -64
- package/godot/addons/scad_importer/LICENSE +0 -21
package/src/cli-utils.js
CHANGED
|
@@ -117,7 +117,33 @@ export function askQuestion(message) {
|
|
|
117
117
|
});
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
export function
|
|
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
|
|
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
|
-
|
|
212
|
-
iterations
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
262
|
+
const body = {
|
|
263
|
+
messages: messages,
|
|
264
|
+
};
|
|
219
265
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
266
|
+
if (modelName) {
|
|
267
|
+
body.model = modelName;
|
|
268
|
+
}
|
|
223
269
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
270
|
+
if (openaiTools.length > 0) {
|
|
271
|
+
body.tools = openaiTools;
|
|
272
|
+
}
|
|
227
273
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
271
|
-
|
|
320
|
+
throw new Error(
|
|
321
|
+
`AI API Error: ${response.status} ${response.statusText}\n${errorText}`,
|
|
322
|
+
);
|
|
272
323
|
}
|
|
273
324
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
325
|
+
const data = await response.json();
|
|
326
|
+
const choice = data.choices[0];
|
|
327
|
+
const message = choice.message;
|
|
278
328
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
329
|
+
const reasoning = message.reasoning_content || message.reasoning || "";
|
|
330
|
+
if (reasoning) {
|
|
331
|
+
logAIThinking(reasoning, isVerbose);
|
|
332
|
+
}
|
|
282
333
|
|
|
283
|
-
|
|
284
|
-
|
|
334
|
+
// Append the assistant message exactly as provided (including tool_calls)
|
|
335
|
+
messages.push(message);
|
|
285
336
|
|
|
286
|
-
|
|
287
|
-
|
|
337
|
+
if (message.tool_calls && message.tool_calls.length > 0) {
|
|
338
|
+
const toolImagesUserContent = [];
|
|
288
339
|
|
|
289
|
-
|
|
290
|
-
|
|
340
|
+
for (const toolCall of message.tool_calls) {
|
|
341
|
+
console.log(`\n⚙️ AI is using tool: ${toolCall.function.name}`);
|
|
291
342
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
-
|
|
329
|
-
|
|
382
|
+
if (disableImageSupport) {
|
|
383
|
+
callArgs.return_images = false;
|
|
384
|
+
}
|
|
330
385
|
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
417
|
+
messages.push({
|
|
418
|
+
role: "tool",
|
|
419
|
+
tool_call_id: toolCall.id,
|
|
420
|
+
content: responseText || "Success",
|
|
421
|
+
});
|
|
351
422
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
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: "
|
|
393
|
-
|
|
394
|
-
content: `Error: ${err.message}`,
|
|
453
|
+
role: "user",
|
|
454
|
+
content: toolImagesUserContent,
|
|
395
455
|
});
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
|
436
|
-
|
|
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
|
-
|
|
442
|
-
|
|
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
|
-
|
|
450
|
-
|
|
470
|
+
const match = finalText.match(
|
|
471
|
+
/```(?:openscad|scad)?\n([\s\S]*?)```/i,
|
|
451
472
|
);
|
|
452
|
-
|
|
473
|
+
finalCode = match ? match[1].trim() : finalText.trim();
|
|
453
474
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
460
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
566
|
+
console.log(` Project folder: ${projectDir}`);
|
|
528
567
|
|
|
529
|
-
|
|
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
|
-
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
581
|
-
|
|
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
|
-
|
|
591
|
-
|
|
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
|
-
|
|
630
|
+
`\n🎉 Success! Final file finalized and written to: ${extractedFilename}`,
|
|
597
631
|
);
|
|
598
|
-
}
|
|
599
632
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
606
|
-
|
|
607
|
-
console.
|
|
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: `
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
-
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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(
|
|
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
|
-
|
|
758
|
-
|
|
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
|
-
|
|
770
|
-
|
|
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(
|
|
806
|
+
await writeToClipboard(combinedOutput);
|
|
777
807
|
console.log(
|
|
778
|
-
"✔️
|
|
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
|
|
811
|
+
console.error("Error: Failed to copy to the clipboard.");
|
|
782
812
|
console.error(err.message);
|
|
783
813
|
process.exit(1);
|
|
784
814
|
}
|