cyclecad 0.2.2 → 0.2.3

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 (69) hide show
  1. package/API-BUILD-MANIFEST.txt +339 -0
  2. package/API-SERVER.md +535 -0
  3. package/Architecture-Deck.pptx +0 -0
  4. package/CLAUDE.md +172 -11
  5. package/CLI-BUILD-SUMMARY.md +504 -0
  6. package/CLI-INDEX.md +356 -0
  7. package/CLI-README.md +466 -0
  8. package/COLLABORATION-INTEGRATION-GUIDE.md +325 -0
  9. package/CONNECTED_FABS_GUIDE.md +612 -0
  10. package/CONNECTED_FABS_README.md +310 -0
  11. package/DELIVERABLES.md +343 -0
  12. package/DFM-ANALYZER-INTEGRATION.md +368 -0
  13. package/DFM-QUICK-START.js +253 -0
  14. package/Dockerfile +69 -0
  15. package/IMPLEMENTATION.md +327 -0
  16. package/LICENSE +31 -0
  17. package/MARKETPLACE_QUICK_REFERENCE.txt +294 -0
  18. package/MCP-INDEX.md +264 -0
  19. package/QUICKSTART-API.md +388 -0
  20. package/QUICKSTART-CLI.md +211 -0
  21. package/QUICKSTART-MCP.md +196 -0
  22. package/README-MCP.md +208 -0
  23. package/TEST-TOKEN-ENGINE.md +319 -0
  24. package/TOKEN-ENGINE-SUMMARY.md +266 -0
  25. package/TOKENS-README.md +263 -0
  26. package/TOOLS-REFERENCE.md +254 -0
  27. package/app/index.html +168 -3
  28. package/app/js/TOKEN-INTEGRATION.md +391 -0
  29. package/app/js/agent-api.js +3 -3
  30. package/app/js/ai-copilot.js +1435 -0
  31. package/app/js/cam-pipeline.js +840 -0
  32. package/app/js/collaboration-ui.js +995 -0
  33. package/app/js/collaboration.js +1116 -0
  34. package/app/js/connected-fabs-example.js +404 -0
  35. package/app/js/connected-fabs.js +1449 -0
  36. package/app/js/dfm-analyzer.js +1760 -0
  37. package/app/js/marketplace.js +1994 -0
  38. package/app/js/material-library.js +2115 -0
  39. package/app/js/token-dashboard.js +563 -0
  40. package/app/js/token-engine.js +743 -0
  41. package/app/test-agent.html +1801 -0
  42. package/bin/cyclecad-cli.js +662 -0
  43. package/bin/cyclecad-mcp +2 -0
  44. package/bin/server.js +242 -0
  45. package/cycleCAD-Architecture.pptx +0 -0
  46. package/cycleCAD-Investor-Deck.pptx +0 -0
  47. package/demo-mcp.sh +60 -0
  48. package/docs/API-SERVER-SUMMARY.md +375 -0
  49. package/docs/API-SERVER.md +667 -0
  50. package/docs/CAM-EXAMPLES.md +344 -0
  51. package/docs/CAM-INTEGRATION.md +612 -0
  52. package/docs/CAM-QUICK-REFERENCE.md +199 -0
  53. package/docs/CLI-INTEGRATION.md +510 -0
  54. package/docs/CLI.md +872 -0
  55. package/docs/MARKETPLACE-API-SCHEMA.json +564 -0
  56. package/docs/MARKETPLACE-INTEGRATION.md +467 -0
  57. package/docs/MARKETPLACE-SETUP.html +439 -0
  58. package/docs/MCP-SERVER.md +403 -0
  59. package/examples/api-client-example.js +488 -0
  60. package/examples/api-client-example.py +359 -0
  61. package/examples/batch-manufacturing.txt +28 -0
  62. package/examples/batch-simple.txt +26 -0
  63. package/model-marketplace.html +1273 -0
  64. package/package.json +14 -3
  65. package/server/api-server.js +1120 -0
  66. package/server/mcp-server.js +1161 -0
  67. package/test-api-server.js +432 -0
  68. package/test-mcp.js +198 -0
  69. package/~$cycleCAD-Investor-Deck.pptx +0 -0
@@ -0,0 +1,1120 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * cycleCAD API Server v0.2.0
5
+ *
6
+ * REST API server for cycleCAD that exposes the Agent API via HTTP.
7
+ * Enables any language/platform to drive cycleCAD through JSON-RPC style endpoints.
8
+ *
9
+ * Features:
10
+ * - POST /api/execute — Execute single Agent API command
11
+ * - GET /api/schema — Introspect full API schema
12
+ * - POST /api/batch — Execute multiple commands
13
+ * - GET /api/history — View command history
14
+ * - GET /api/health — Health check
15
+ * - WebSocket /api/ws — Bidirectional real-time connection
16
+ * - Static file serving for cycleCAD web app
17
+ * - Rate limiting (100 req/min per IP)
18
+ * - API key authentication (optional)
19
+ *
20
+ * Zero external dependencies — uses only Node.js built-ins.
21
+ */
22
+
23
+ const http = require('http');
24
+ const https = require('https');
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+ const url = require('url');
28
+ const crypto = require('crypto');
29
+ const { EventEmitter } = require('events');
30
+
31
+ // ============================================================================
32
+ // CONFIGURATION
33
+ // ============================================================================
34
+
35
+ const PORT = process.env.PORT || 3000;
36
+ const HOST = process.env.HOST || '0.0.0.0';
37
+ const API_KEY = process.env.CYCLECAD_API_KEY || null; // Optional auth
38
+ const STATIC_DIR = process.env.STATIC_DIR || path.join(__dirname, '../app');
39
+ const ENABLE_HTTPS = process.env.ENABLE_HTTPS === 'true';
40
+ const CERT_FILE = process.env.CERT_FILE || '';
41
+ const KEY_FILE = process.env.KEY_FILE || '';
42
+ const DEV_MODE = process.argv.includes('--dev');
43
+
44
+ // ============================================================================
45
+ // RATE LIMITER
46
+ // ============================================================================
47
+
48
+ class RateLimiter {
49
+ constructor(maxRequests = 100, windowMs = 60000) {
50
+ this.maxRequests = maxRequests;
51
+ this.windowMs = windowMs;
52
+ this.requests = new Map(); // ip -> [timestamps]
53
+ }
54
+
55
+ isAllowed(ip) {
56
+ const now = Date.now();
57
+ if (!this.requests.has(ip)) {
58
+ this.requests.set(ip, []);
59
+ }
60
+
61
+ const timestamps = this.requests.get(ip);
62
+
63
+ // Remove old entries outside the window
64
+ const validTimestamps = timestamps.filter(t => now - t < this.windowMs);
65
+ this.requests.set(ip, validTimestamps);
66
+
67
+ if (validTimestamps.length >= this.maxRequests) {
68
+ return false;
69
+ }
70
+
71
+ validTimestamps.push(now);
72
+ return true;
73
+ }
74
+
75
+ remaining(ip) {
76
+ const now = Date.now();
77
+ const timestamps = this.requests.get(ip) || [];
78
+ const validTimestamps = timestamps.filter(t => now - t < this.windowMs);
79
+ return Math.max(0, this.maxRequests - validTimestamps.length);
80
+ }
81
+ }
82
+
83
+ const limiter = new RateLimiter(100, 60000);
84
+
85
+ // ============================================================================
86
+ // API SERVER STATE
87
+ // ============================================================================
88
+
89
+ class APIServer extends EventEmitter {
90
+ constructor() {
91
+ super();
92
+ this.sessionId = crypto.randomUUID();
93
+ this.startTime = Date.now();
94
+ this.commandLog = [];
95
+ this.features = [];
96
+ this.models = [];
97
+ this.wsClients = new Set();
98
+ this.tokenBuckets = new Map(); // IP -> { tokens, lastRefill }
99
+ }
100
+
101
+ executeCommand(cmd) {
102
+ const start = performance.now();
103
+
104
+ try {
105
+ if (!cmd || !cmd.method) {
106
+ return this._err('Missing "method" field in command');
107
+ }
108
+
109
+ const handler = AGENT_COMMANDS[cmd.method];
110
+ if (!handler) {
111
+ const suggestions = this._suggestMethod(cmd.method);
112
+ return this._err(
113
+ `Unknown method: "${cmd.method}".` +
114
+ (suggestions.length > 0 ? ` Did you mean: ${suggestions.join(', ')}?` : '') +
115
+ ` Use GET /api/schema to see available commands.`
116
+ );
117
+ }
118
+
119
+ const result = handler.call(this, cmd.params || {});
120
+ const elapsed = Math.round(performance.now() - start);
121
+
122
+ // Log command
123
+ const entry = {
124
+ id: `cmd_${this.commandLog.length}`,
125
+ method: cmd.method,
126
+ params: cmd.params,
127
+ elapsed,
128
+ ok: true,
129
+ timestamp: new Date().toISOString(),
130
+ result: typeof result === 'object' ? Object.keys(result).slice(0, 3) : String(result).slice(0, 100)
131
+ };
132
+ this.commandLog.push(entry);
133
+
134
+ // Broadcast to WebSocket clients
135
+ this._broadcastWS({
136
+ type: 'commandExecuted',
137
+ method: cmd.method,
138
+ elapsed,
139
+ ok: true
140
+ });
141
+
142
+ return {
143
+ ok: true,
144
+ result,
145
+ elapsed,
146
+ sessionId: this.sessionId
147
+ };
148
+ } catch (e) {
149
+ const elapsed = Math.round(performance.now() - start);
150
+ const entry = {
151
+ id: `cmd_${this.commandLog.length}`,
152
+ method: cmd.method,
153
+ params: cmd.params,
154
+ elapsed,
155
+ ok: false,
156
+ error: e.message,
157
+ timestamp: new Date().toISOString()
158
+ };
159
+ this.commandLog.push(entry);
160
+
161
+ this._broadcastWS({
162
+ type: 'commandFailed',
163
+ method: cmd.method,
164
+ error: e.message,
165
+ elapsed
166
+ });
167
+
168
+ return this._err(e.message, elapsed);
169
+ }
170
+ }
171
+
172
+ executeBatch(commands) {
173
+ const results = [];
174
+ const errors = [];
175
+ const start = performance.now();
176
+
177
+ for (let i = 0; i < commands.length; i++) {
178
+ const r = this.executeCommand(commands[i]);
179
+ results.push(r);
180
+ if (!r.ok) {
181
+ errors.push({ index: i, method: commands[i].method, error: r.error });
182
+ }
183
+ }
184
+
185
+ const elapsed = Math.round(performance.now() - start);
186
+
187
+ return {
188
+ ok: errors.length === 0,
189
+ results,
190
+ errors,
191
+ executed: results.length - errors.length,
192
+ total: commands.length,
193
+ elapsed,
194
+ sessionId: this.sessionId
195
+ };
196
+ }
197
+
198
+ getSchema() {
199
+ const schema = {};
200
+
201
+ for (const [method, handler] of Object.entries(AGENT_COMMANDS)) {
202
+ const [namespace, command] = method.split('.');
203
+ if (!schema[namespace]) {
204
+ schema[namespace] = { description: NAMESPACE_DESCRIPTIONS[namespace] || '', commands: {} };
205
+ }
206
+
207
+ schema[namespace].commands[command] = {
208
+ method,
209
+ description: handler.description || 'No description',
210
+ params: handler.params || {},
211
+ returns: handler.returns || {}
212
+ };
213
+ }
214
+
215
+ return {
216
+ version: '0.2.0',
217
+ sessionId: this.sessionId,
218
+ namespaces: schema,
219
+ totalCommands: Object.keys(AGENT_COMMANDS).length
220
+ };
221
+ }
222
+
223
+ getHistory(count = 20) {
224
+ return {
225
+ sessionId: this.sessionId,
226
+ total: this.commandLog.length,
227
+ recent: this.commandLog.slice(-count),
228
+ timestamp: new Date().toISOString()
229
+ };
230
+ }
231
+
232
+ getHealth() {
233
+ return {
234
+ status: 'ok',
235
+ version: '0.2.0',
236
+ uptime: Math.round((Date.now() - this.startTime) / 1000),
237
+ sessionId: this.sessionId,
238
+ commands: Object.keys(AGENT_COMMANDS).length,
239
+ commandsExecuted: this.commandLog.length,
240
+ features: this.features.length,
241
+ models: this.models.length,
242
+ wsClients: this.wsClients.size,
243
+ timestamp: new Date().toISOString()
244
+ };
245
+ }
246
+
247
+ addWSClient(ws) {
248
+ this.wsClients.add(ws);
249
+ return this.wsClients.size;
250
+ }
251
+
252
+ removeWSClient(ws) {
253
+ this.wsClients.delete(ws);
254
+ return this.wsClients.size;
255
+ }
256
+
257
+ _broadcastWS(message) {
258
+ const data = JSON.stringify(message);
259
+ for (const ws of this.wsClients) {
260
+ try {
261
+ ws.send(data);
262
+ } catch (e) {
263
+ // Silently ignore errors
264
+ }
265
+ }
266
+ }
267
+
268
+ _err(msg, elapsed = 0) {
269
+ return {
270
+ ok: false,
271
+ error: msg,
272
+ elapsed,
273
+ sessionId: this.sessionId
274
+ };
275
+ }
276
+
277
+ _suggestMethod(invalid) {
278
+ const allMethods = Object.keys(AGENT_COMMANDS);
279
+ const suggestions = allMethods
280
+ .filter(m => this._editDistance(invalid, m) <= 3)
281
+ .slice(0, 3);
282
+ return suggestions;
283
+ }
284
+
285
+ _editDistance(a, b) {
286
+ if (a.length === 0) return b.length;
287
+ if (b.length === 0) return a.length;
288
+ const matrix = Array(b.length + 1).fill(null).map(() => Array(a.length + 1).fill(0));
289
+ for (let i = 0; i <= a.length; i++) matrix[0][i] = i;
290
+ for (let j = 0; j <= b.length; j++) matrix[j][0] = j;
291
+ for (let j = 1; j <= b.length; j++) {
292
+ for (let i = 1; i <= a.length; i++) {
293
+ const indicator = a[i - 1] === b[j - 1] ? 0 : 1;
294
+ matrix[j][i] = Math.min(
295
+ matrix[j][i - 1] + 1,
296
+ matrix[j - 1][i] + 1,
297
+ matrix[j - 1][i - 1] + indicator
298
+ );
299
+ }
300
+ }
301
+ return matrix[b.length][a.length];
302
+ }
303
+ }
304
+
305
+ const server = new APIServer();
306
+
307
+ // ============================================================================
308
+ // AGENT COMMANDS (Mock implementations for server-side)
309
+ // ============================================================================
310
+
311
+ const AGENT_COMMANDS = {
312
+ // SKETCH commands
313
+ 'sketch.start': {
314
+ description: 'Start a 2D sketch on a plane',
315
+ params: { plane: 'string (XY|XZ|YZ)' },
316
+ returns: { sketchId: 'string', plane: 'string', status: 'string' },
317
+ handler(params) {
318
+ const plane = params.plane || 'XY';
319
+ if (!['XY', 'XZ', 'YZ'].includes(plane)) {
320
+ throw new Error(`Invalid plane: ${plane}`);
321
+ }
322
+ return {
323
+ sketchId: `sketch_${Date.now()}`,
324
+ plane,
325
+ status: 'active',
326
+ message: `Sketch started on ${plane} plane`
327
+ };
328
+ }
329
+ },
330
+
331
+ 'sketch.line': {
332
+ description: 'Draw a line segment',
333
+ params: { x1: 'number', y1: 'number', x2: 'number', y2: 'number' },
334
+ returns: { entityId: 'string', type: 'string', length: 'number' },
335
+ handler(params) {
336
+ const dx = params.x2 - params.x1;
337
+ const dy = params.y2 - params.y1;
338
+ const length = Math.sqrt(dx * dx + dy * dy);
339
+ return { entityId: `line_${Date.now()}`, type: 'line', length };
340
+ }
341
+ },
342
+
343
+ 'sketch.circle': {
344
+ description: 'Draw a circle',
345
+ params: { cx: 'number', cy: 'number', radius: 'number' },
346
+ returns: { entityId: 'string', type: 'string', radius: 'number' },
347
+ handler(params) {
348
+ return {
349
+ entityId: `circle_${Date.now()}`,
350
+ type: 'circle',
351
+ radius: params.radius,
352
+ center: [params.cx, params.cy],
353
+ area: Math.PI * params.radius * params.radius
354
+ };
355
+ }
356
+ },
357
+
358
+ 'sketch.rect': {
359
+ description: 'Draw a rectangle',
360
+ params: { x: 'number', y: 'number', width: 'number', height: 'number' },
361
+ returns: { entityId: 'string', type: 'string', area: 'number' },
362
+ handler(params) {
363
+ return {
364
+ entityId: `rect_${Date.now()}`,
365
+ type: 'rect',
366
+ x: params.x,
367
+ y: params.y,
368
+ width: params.width,
369
+ height: params.height,
370
+ area: params.width * params.height
371
+ };
372
+ }
373
+ },
374
+
375
+ 'sketch.end': {
376
+ description: 'End the sketch',
377
+ params: {},
378
+ returns: { status: 'string', message: 'string' },
379
+ handler(params) {
380
+ return { status: 'complete', message: 'Sketch ended' };
381
+ }
382
+ },
383
+
384
+ // OPERATIONS commands
385
+ 'ops.extrude': {
386
+ description: 'Extrude a sketch into 3D',
387
+ params: { height: 'number', symmetric: 'boolean', material: 'string' },
388
+ returns: { featureId: 'string', type: 'string', volume: 'number' },
389
+ handler(params) {
390
+ const volume = params.height * 100; // Mock calculation
391
+ return {
392
+ featureId: `extrude_${Date.now()}`,
393
+ type: 'extrude',
394
+ height: params.height,
395
+ symmetric: params.symmetric || false,
396
+ material: params.material || 'steel',
397
+ volume
398
+ };
399
+ }
400
+ },
401
+
402
+ 'ops.fillet': {
403
+ description: 'Fillet edges of a feature',
404
+ params: { target: 'string', radius: 'number' },
405
+ returns: { featureId: 'string', radius: 'number' },
406
+ handler(params) {
407
+ return {
408
+ featureId: `fillet_${Date.now()}`,
409
+ target: params.target,
410
+ radius: params.radius
411
+ };
412
+ }
413
+ },
414
+
415
+ 'ops.chamfer': {
416
+ description: 'Chamfer edges of a feature',
417
+ params: { target: 'string', distance: 'number' },
418
+ returns: { featureId: 'string', distance: 'number' },
419
+ handler(params) {
420
+ return {
421
+ featureId: `chamfer_${Date.now()}`,
422
+ target: params.target,
423
+ distance: params.distance
424
+ };
425
+ }
426
+ },
427
+
428
+ 'ops.hole': {
429
+ description: 'Create a hole',
430
+ params: { radius: 'number', depth: 'number' },
431
+ returns: { featureId: 'string', radius: 'number', depth: 'number' },
432
+ handler(params) {
433
+ return {
434
+ featureId: `hole_${Date.now()}`,
435
+ radius: params.radius,
436
+ depth: params.depth,
437
+ type: 'hole'
438
+ };
439
+ }
440
+ },
441
+
442
+ 'ops.pattern': {
443
+ description: 'Create a rectangular or circular pattern',
444
+ params: { target: 'string', type: 'string', count: 'number', spacing: 'number' },
445
+ returns: { featureId: 'string', count: 'number' },
446
+ handler(params) {
447
+ return {
448
+ featureId: `pattern_${Date.now()}`,
449
+ target: params.target,
450
+ type: params.type || 'rect',
451
+ count: params.count,
452
+ spacing: params.spacing
453
+ };
454
+ }
455
+ },
456
+
457
+ // VIEW commands
458
+ 'view.set': {
459
+ description: 'Set viewport view (isometric, top, front, right, bottom, back, left)',
460
+ params: { view: 'string' },
461
+ returns: { view: 'string', message: 'string' },
462
+ handler(params) {
463
+ const validViews = ['isometric', 'top', 'front', 'right', 'bottom', 'back', 'left'];
464
+ if (!validViews.includes(params.view)) {
465
+ throw new Error(`Invalid view: ${params.view}`);
466
+ }
467
+ return { view: params.view, message: `View set to ${params.view}` };
468
+ }
469
+ },
470
+
471
+ 'view.grid': {
472
+ description: 'Toggle grid visibility',
473
+ params: { visible: 'boolean' },
474
+ returns: { visible: 'boolean', message: 'string' },
475
+ handler(params) {
476
+ return {
477
+ visible: params.visible !== false,
478
+ message: `Grid ${params.visible !== false ? 'enabled' : 'disabled'}`
479
+ };
480
+ }
481
+ },
482
+
483
+ 'view.wireframe': {
484
+ description: 'Toggle wireframe mode',
485
+ params: { enabled: 'boolean' },
486
+ returns: { enabled: 'boolean', message: 'string' },
487
+ handler(params) {
488
+ return {
489
+ enabled: params.enabled !== false,
490
+ message: `Wireframe ${params.enabled !== false ? 'enabled' : 'disabled'}`
491
+ };
492
+ }
493
+ },
494
+
495
+ // EXPORT commands
496
+ 'export.stl': {
497
+ description: 'Export to STL format',
498
+ params: { filename: 'string', binary: 'boolean' },
499
+ returns: { filename: 'string', format: 'string', message: 'string' },
500
+ handler(params) {
501
+ return {
502
+ filename: params.filename || 'output.stl',
503
+ format: params.binary ? 'binary' : 'ascii',
504
+ bytes: Math.floor(Math.random() * 10000000),
505
+ message: 'Export would save to file'
506
+ };
507
+ }
508
+ },
509
+
510
+ 'export.obj': {
511
+ description: 'Export to OBJ format',
512
+ params: { filename: 'string' },
513
+ returns: { filename: 'string', format: 'string' },
514
+ handler(params) {
515
+ return {
516
+ filename: params.filename || 'output.obj',
517
+ format: 'obj',
518
+ message: 'Export would save to file'
519
+ };
520
+ }
521
+ },
522
+
523
+ 'export.gltf': {
524
+ description: 'Export to glTF format',
525
+ params: { filename: 'string' },
526
+ returns: { filename: 'string', format: 'string' },
527
+ handler(params) {
528
+ return {
529
+ filename: params.filename || 'output.gltf',
530
+ format: 'gltf',
531
+ message: 'Export would save to file'
532
+ };
533
+ }
534
+ },
535
+
536
+ // QUERY commands
537
+ 'query.features': {
538
+ description: 'List all features in the model',
539
+ params: {},
540
+ returns: { features: 'array', count: 'number' },
541
+ handler(params) {
542
+ return {
543
+ features: server.features,
544
+ count: server.features.length
545
+ };
546
+ }
547
+ },
548
+
549
+ 'query.bbox': {
550
+ description: 'Get bounding box of a feature',
551
+ params: { target: 'string' },
552
+ returns: { min: 'array', max: 'array', size: 'array' },
553
+ handler(params) {
554
+ return {
555
+ target: params.target,
556
+ min: [-50, -50, -50],
557
+ max: [50, 50, 50],
558
+ size: [100, 100, 100]
559
+ };
560
+ }
561
+ },
562
+
563
+ 'query.materials': {
564
+ description: 'List available materials',
565
+ params: {},
566
+ returns: { materials: 'array' },
567
+ handler(params) {
568
+ return {
569
+ materials: ['steel', 'aluminum', 'brass', 'plastic', 'titanium', 'carbon-fiber', 'rubber', 'wood']
570
+ };
571
+ }
572
+ },
573
+
574
+ // VALIDATE commands
575
+ 'validate.dimensions': {
576
+ description: 'Check feature dimensions',
577
+ params: { target: 'string' },
578
+ returns: { target: 'string', length: 'number', width: 'number', height: 'number' },
579
+ handler(params) {
580
+ return {
581
+ target: params.target,
582
+ length: 100,
583
+ width: 50,
584
+ height: 75,
585
+ message: 'Dimensions OK'
586
+ };
587
+ }
588
+ },
589
+
590
+ 'validate.cost': {
591
+ description: 'Estimate manufacturing cost',
592
+ params: { target: 'string', process: 'string', material: 'string' },
593
+ returns: { target: 'string', process: 'string', estimatedCost: 'number' },
594
+ handler(params) {
595
+ const costMap = { 'FDM': 15, 'SLA': 25, 'CNC': 50, 'injection': 100 };
596
+ const cost = costMap[params.process] || 20;
597
+ return {
598
+ target: params.target,
599
+ process: params.process || 'FDM',
600
+ material: params.material || 'PLA',
601
+ estimatedCost: cost,
602
+ currency: 'USD'
603
+ };
604
+ }
605
+ },
606
+
607
+ 'validate.mass': {
608
+ description: 'Calculate feature mass',
609
+ params: { target: 'string', material: 'string' },
610
+ returns: { target: 'string', mass: 'number', material: 'string' },
611
+ handler(params) {
612
+ const densities = { steel: 7.85, aluminum: 2.70, brass: 8.56, titanium: 4.5 };
613
+ const density = densities[params.material] || 7.85;
614
+ return {
615
+ target: params.target,
616
+ mass: Math.round(density * 100) / 100,
617
+ unit: 'kg',
618
+ material: params.material || 'steel'
619
+ };
620
+ }
621
+ },
622
+
623
+ // ASSEMBLY commands
624
+ 'assembly.addComponent': {
625
+ description: 'Add a component to the assembly',
626
+ params: { name: 'string', position: 'array' },
627
+ returns: { componentId: 'string', name: 'string' },
628
+ handler(params) {
629
+ const comp = {
630
+ id: `comp_${Date.now()}`,
631
+ name: params.name || 'Component',
632
+ position: params.position || [0, 0, 0]
633
+ };
634
+ server.models.push(comp);
635
+ return comp;
636
+ }
637
+ },
638
+
639
+ 'assembly.list': {
640
+ description: 'List all components in assembly',
641
+ params: {},
642
+ returns: { components: 'array', count: 'number' },
643
+ handler(params) {
644
+ return {
645
+ components: server.models,
646
+ count: server.models.length
647
+ };
648
+ }
649
+ },
650
+
651
+ // META commands
652
+ 'meta.ping': {
653
+ description: 'Ping the server',
654
+ params: {},
655
+ returns: { status: 'string', timestamp: 'string' },
656
+ handler(params) {
657
+ return {
658
+ status: 'pong',
659
+ timestamp: new Date().toISOString()
660
+ };
661
+ }
662
+ },
663
+
664
+ 'meta.version': {
665
+ description: 'Get server and API version',
666
+ params: {},
667
+ returns: { version: 'string', name: 'string' },
668
+ handler(params) {
669
+ return {
670
+ name: 'cycleCAD API Server',
671
+ version: '0.2.0',
672
+ nodeVersion: process.version
673
+ };
674
+ }
675
+ },
676
+
677
+ 'meta.schema': {
678
+ description: 'Get API schema',
679
+ params: {},
680
+ returns: { schema: 'object' },
681
+ handler(params) {
682
+ return server.getSchema();
683
+ }
684
+ }
685
+ };
686
+
687
+ // Bind handlers
688
+ for (const [key, cmd] of Object.entries(AGENT_COMMANDS)) {
689
+ if (cmd.handler) {
690
+ // Create a wrapper that calls handler with 'this' bound to server
691
+ const originalHandler = cmd.handler;
692
+ cmd.handler = function(params) {
693
+ return originalHandler.call(this, params);
694
+ };
695
+ }
696
+ }
697
+
698
+ const NAMESPACE_DESCRIPTIONS = {
699
+ sketch: 'Create 2D sketches on planes',
700
+ ops: 'Perform 3D operations (extrude, fillet, etc)',
701
+ view: 'Control viewport and visualization',
702
+ export: 'Export to various file formats',
703
+ query: 'Query model data',
704
+ validate: 'Validate and analyze models',
705
+ assembly: 'Manage assemblies and components',
706
+ meta: 'Server metadata and introspection'
707
+ };
708
+
709
+ // ============================================================================
710
+ // HTTP SERVER
711
+ // ============================================================================
712
+
713
+ function createServer() {
714
+ return http.createServer((req, res) => {
715
+ const clientIp = req.headers['x-forwarded-for']?.split(',')[0].trim() || req.socket.remoteAddress;
716
+
717
+ // CORS headers
718
+ res.setHeader('Access-Control-Allow-Origin', '*');
719
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE, PUT');
720
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-API-Key, Authorization');
721
+ res.setHeader('Access-Control-Max-Age', '3600');
722
+
723
+ // COOP/COEP for SharedArrayBuffer
724
+ res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
725
+ res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
726
+
727
+ // Handle OPTIONS
728
+ if (req.method === 'OPTIONS') {
729
+ res.writeHead(204);
730
+ res.end();
731
+ return;
732
+ }
733
+
734
+ // Rate limiting
735
+ if (!limiter.isAllowed(clientIp)) {
736
+ respondJSON(res, 429, {
737
+ ok: false,
738
+ error: 'Too many requests',
739
+ retryAfter: 60
740
+ });
741
+ return;
742
+ }
743
+
744
+ res.setHeader('RateLimit-Remaining', limiter.remaining(clientIp));
745
+ res.setHeader('RateLimit-Limit', '100');
746
+ res.setHeader('RateLimit-Reset', Math.ceil(Date.now() / 1000) + 60);
747
+
748
+ // API key auth
749
+ if (API_KEY) {
750
+ const providedKey = req.headers['x-api-key'] || new url.URL(req.url, `http://${req.headers.host}`).searchParams.get('api_key');
751
+ if (providedKey !== API_KEY) {
752
+ respondJSON(res, 401, {
753
+ ok: false,
754
+ error: 'Unauthorized - invalid or missing API key'
755
+ });
756
+ return;
757
+ }
758
+ }
759
+
760
+ const parsedUrl = new url.URL(req.url, `http://${req.headers.host}`);
761
+ const pathname = parsedUrl.pathname;
762
+
763
+ // === ROUTING ===
764
+
765
+ if (pathname === '/api/execute' && req.method === 'POST') {
766
+ handleExecute(req, res);
767
+ } else if (pathname === '/api/batch' && req.method === 'POST') {
768
+ handleBatch(req, res);
769
+ } else if (pathname === '/api/schema' && req.method === 'GET') {
770
+ respondJSON(res, 200, server.getSchema());
771
+ } else if (pathname === '/api/health' && req.method === 'GET') {
772
+ respondJSON(res, 200, server.getHealth());
773
+ } else if (pathname === '/api/history' && req.method === 'GET') {
774
+ const count = parseInt(parsedUrl.searchParams.get('count') || '20', 10);
775
+ respondJSON(res, 200, server.getHistory(count));
776
+ } else if (pathname === '/api/models' && req.method === 'GET') {
777
+ respondJSON(res, 200, {
778
+ ok: true,
779
+ models: server.models,
780
+ count: server.models.length
781
+ });
782
+ } else if (pathname.match(/^\/api\/models\/[^\/]+$/) && req.method === 'GET') {
783
+ const id = pathname.split('/').pop();
784
+ const model = server.models.find(m => m.id === id);
785
+ if (!model) {
786
+ respondJSON(res, 404, { ok: false, error: 'Model not found' });
787
+ } else {
788
+ respondJSON(res, 200, { ok: true, model });
789
+ }
790
+ } else if (pathname.match(/^\/api\/models\/[^\/]+$/) && req.method === 'DELETE') {
791
+ const id = pathname.split('/').pop();
792
+ server.models = server.models.filter(m => m.id !== id);
793
+ respondJSON(res, 200, {
794
+ ok: true,
795
+ message: `Model ${id} deleted`,
796
+ remaining: server.models.length
797
+ });
798
+ } else if (pathname === '/api/ws' && req.headers.upgrade === 'websocket') {
799
+ handleWebSocket(req);
800
+ } else if (pathname === '/') {
801
+ serveFile(res, path.join(STATIC_DIR, 'index.html'));
802
+ } else if (pathname === '/app' || pathname === '/app/') {
803
+ serveFile(res, path.join(STATIC_DIR, 'index.html'));
804
+ } else if (pathname.startsWith('/api/')) {
805
+ respondJSON(res, 404, {
806
+ ok: false,
807
+ error: 'Unknown API endpoint',
808
+ endpoint: pathname,
809
+ availableEndpoints: [
810
+ 'POST /api/execute',
811
+ 'POST /api/batch',
812
+ 'GET /api/schema',
813
+ 'GET /api/health',
814
+ 'GET /api/history',
815
+ 'GET /api/models',
816
+ 'GET /api/models/:id',
817
+ 'DELETE /api/models/:id',
818
+ 'WebSocket /api/ws'
819
+ ]
820
+ });
821
+ } else {
822
+ // Serve static files
823
+ const filePath = path.join(STATIC_DIR, pathname);
824
+ serveFile(res, filePath);
825
+ }
826
+ });
827
+ }
828
+
829
+ // ============================================================================
830
+ // REQUEST HANDLERS
831
+ // ============================================================================
832
+
833
+ function handleExecute(req, res) {
834
+ let body = '';
835
+
836
+ req.on('data', chunk => {
837
+ body += chunk.toString();
838
+ });
839
+
840
+ req.on('end', () => {
841
+ try {
842
+ const cmd = JSON.parse(body);
843
+ const result = server.executeCommand(cmd);
844
+ respondJSON(res, result.ok ? 200 : 400, result);
845
+ } catch (e) {
846
+ respondJSON(res, 400, {
847
+ ok: false,
848
+ error: `Invalid JSON: ${e.message}`
849
+ });
850
+ }
851
+ });
852
+ }
853
+
854
+ function handleBatch(req, res) {
855
+ let body = '';
856
+
857
+ req.on('data', chunk => {
858
+ body += chunk.toString();
859
+ });
860
+
861
+ req.on('end', () => {
862
+ try {
863
+ const payload = JSON.parse(body);
864
+ if (!Array.isArray(payload.commands)) {
865
+ return respondJSON(res, 400, {
866
+ ok: false,
867
+ error: 'Expected { commands: [{ method, params }] }'
868
+ });
869
+ }
870
+
871
+ const result = server.executeBatch(payload.commands);
872
+ respondJSON(res, result.ok ? 200 : 400, result);
873
+ } catch (e) {
874
+ respondJSON(res, 400, {
875
+ ok: false,
876
+ error: `Invalid JSON: ${e.message}`
877
+ });
878
+ }
879
+ });
880
+ }
881
+
882
+ function handleWebSocket(req) {
883
+ // Basic WebSocket upgrade (simplified, not full RFC 6455)
884
+ const key = req.headers['sec-websocket-key'];
885
+ const sha1 = crypto.createHash('sha1');
886
+ sha1.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');
887
+ const accept = sha1.digest('base64');
888
+
889
+ const head = Buffer.from(
890
+ `HTTP/1.1 101 Switching Protocols\r\n` +
891
+ `Upgrade: websocket\r\n` +
892
+ `Connection: Upgrade\r\n` +
893
+ `Sec-WebSocket-Accept: ${accept}\r\n` +
894
+ `\r\n`
895
+ );
896
+
897
+ req.socket.write(head);
898
+
899
+ // Create a mock WebSocket client
900
+ const ws = {
901
+ send: (data) => {
902
+ const frame = createWebSocketFrame(data);
903
+ req.socket.write(frame);
904
+ },
905
+ close: () => {
906
+ req.socket.destroy();
907
+ }
908
+ };
909
+
910
+ server.addWSClient(ws);
911
+
912
+ // Send welcome message
913
+ ws.send(JSON.stringify({
914
+ type: 'welcome',
915
+ sessionId: server.sessionId,
916
+ message: 'Connected to cycleCAD API Server'
917
+ }));
918
+
919
+ // Send ping every 30s
920
+ const pingInterval = setInterval(() => {
921
+ try {
922
+ ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() }));
923
+ } catch (e) {
924
+ clearInterval(pingInterval);
925
+ }
926
+ }, 30000);
927
+
928
+ // Parse incoming WebSocket frames (simplified)
929
+ let frameBuffer = Buffer.alloc(0);
930
+
931
+ req.socket.on('data', (chunk) => {
932
+ frameBuffer = Buffer.concat([frameBuffer, chunk]);
933
+
934
+ while (frameBuffer.length >= 2) {
935
+ try {
936
+ const { payload, bytesRead } = parseWebSocketFrame(frameBuffer);
937
+ if (payload === null) break; // Incomplete frame
938
+
939
+ frameBuffer = frameBuffer.slice(bytesRead);
940
+
941
+ if (payload) {
942
+ try {
943
+ const cmd = JSON.parse(payload);
944
+ const result = server.executeCommand(cmd);
945
+ ws.send(JSON.stringify(result));
946
+ } catch (e) {
947
+ ws.send(JSON.stringify({ ok: false, error: e.message }));
948
+ }
949
+ }
950
+ } catch (e) {
951
+ ws.close();
952
+ break;
953
+ }
954
+ }
955
+ });
956
+
957
+ req.socket.on('end', () => {
958
+ clearInterval(pingInterval);
959
+ server.removeWSClient(ws);
960
+ });
961
+
962
+ req.socket.on('error', () => {
963
+ clearInterval(pingInterval);
964
+ server.removeWSClient(ws);
965
+ });
966
+ }
967
+
968
+ // ============================================================================
969
+ // HELPERS
970
+ // ============================================================================
971
+
972
+ function respondJSON(res, statusCode, data) {
973
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
974
+ res.end(JSON.stringify(data, null, 2));
975
+ }
976
+
977
+ function serveFile(res, filePath) {
978
+ // Prevent directory traversal
979
+ const normalizedPath = path.normalize(filePath);
980
+ const normalizedBase = path.normalize(STATIC_DIR);
981
+
982
+ if (!normalizedPath.startsWith(normalizedBase)) {
983
+ res.writeHead(403, { 'Content-Type': 'text/plain' });
984
+ res.end('Forbidden');
985
+ return;
986
+ }
987
+
988
+ fs.stat(filePath, (err, stats) => {
989
+ if (err) {
990
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
991
+ res.end('Not found');
992
+ return;
993
+ }
994
+
995
+ if (stats.isDirectory()) {
996
+ serveFile(res, path.join(filePath, 'index.html'));
997
+ return;
998
+ }
999
+
1000
+ const ext = path.extname(filePath);
1001
+ const mimeTypes = {
1002
+ '.html': 'text/html; charset=utf-8',
1003
+ '.js': 'text/javascript; charset=utf-8',
1004
+ '.css': 'text/css',
1005
+ '.json': 'application/json',
1006
+ '.png': 'image/png',
1007
+ '.jpg': 'image/jpeg',
1008
+ '.gif': 'image/gif',
1009
+ '.svg': 'image/svg+xml',
1010
+ '.wasm': 'application/wasm',
1011
+ '.woff': 'font/woff',
1012
+ '.woff2': 'font/woff2'
1013
+ };
1014
+
1015
+ const contentType = mimeTypes[ext] || 'application/octet-stream';
1016
+
1017
+ res.writeHead(200, {
1018
+ 'Content-Type': contentType,
1019
+ 'Cache-Control': ext === '.html' ? 'no-cache' : 'public, max-age=3600'
1020
+ });
1021
+
1022
+ fs.createReadStream(filePath).pipe(res);
1023
+ });
1024
+ }
1025
+
1026
+ function createWebSocketFrame(data) {
1027
+ const buffer = Buffer.from(data);
1028
+ const frame = Buffer.alloc(buffer.length + 2);
1029
+ frame[0] = 0x81; // FIN + text frame
1030
+ frame[1] = buffer.length;
1031
+ buffer.copy(frame, 2);
1032
+ return frame;
1033
+ }
1034
+
1035
+ function parseWebSocketFrame(buffer) {
1036
+ if (buffer.length < 2) return { payload: null, bytesRead: 0 };
1037
+
1038
+ const opcode = buffer[0] & 0x0f;
1039
+ const masked = (buffer[1] & 0x80) !== 0;
1040
+ let payloadLength = buffer[1] & 0x7f;
1041
+ let offset = 2;
1042
+
1043
+ if (payloadLength === 126) {
1044
+ if (buffer.length < 4) return { payload: null, bytesRead: 0 };
1045
+ payloadLength = buffer.readUInt16BE(2);
1046
+ offset = 4;
1047
+ } else if (payloadLength === 127) {
1048
+ if (buffer.length < 10) return { payload: null, bytesRead: 0 };
1049
+ payloadLength = Number(buffer.readBigUInt64BE(2));
1050
+ offset = 10;
1051
+ }
1052
+
1053
+ const maskOffset = masked ? 4 : 0;
1054
+ const totalLength = offset + maskOffset + payloadLength;
1055
+
1056
+ if (buffer.length < totalLength) return { payload: null, bytesRead: 0 };
1057
+
1058
+ let payload = buffer.slice(offset + maskOffset, offset + maskOffset + payloadLength);
1059
+
1060
+ if (masked) {
1061
+ const mask = buffer.slice(offset, offset + 4);
1062
+ for (let i = 0; i < payload.length; i++) {
1063
+ payload[i] ^= mask[i % 4];
1064
+ }
1065
+ }
1066
+
1067
+ return {
1068
+ payload: opcode === 1 ? payload.toString('utf8') : null,
1069
+ bytesRead: totalLength
1070
+ };
1071
+ }
1072
+
1073
+ // ============================================================================
1074
+ // STARTUP
1075
+ // ============================================================================
1076
+
1077
+ function startup() {
1078
+ const httpServer = createServer();
1079
+
1080
+ httpServer.listen(PORT, HOST, () => {
1081
+ const isDev = DEV_MODE ? ' [DEV MODE]' : '';
1082
+ const banner = `
1083
+ ╔═══════════════════════════════════════════════════════════╗
1084
+ ║ cycleCAD API Server v0.2.0${isDev.padEnd(20)} ║
1085
+ ║ ║
1086
+ ║ HTTP: http://${HOST === '0.0.0.0' ? 'localhost' : HOST}:${PORT} ║
1087
+ ║ API: POST /api/execute ║
1088
+ ║ Batch: POST /api/batch ║
1089
+ ║ Schema: GET /api/schema ║
1090
+ ║ Health: GET /api/health ║
1091
+ ║ History: GET /api/history ║
1092
+ ║ Models: GET /api/models ║
1093
+ ║ WebSocket: ws://${HOST === '0.0.0.0' ? 'localhost' : HOST}:${PORT}/api/ws ║
1094
+ ║ Static: ${STATIC_DIR.slice(-30).padEnd(30)} ║
1095
+ ║ ║
1096
+ ║ Rate Limit: 100 requests/minute ║
1097
+ ║ Session ID: ${server.sessionId.slice(0, 50).padEnd(50)} ║
1098
+ ╚═══════════════════════════════════════════════════════════╝
1099
+ `;
1100
+ console.log(banner);
1101
+
1102
+ if (DEV_MODE) {
1103
+ console.log('✓ Development mode enabled');
1104
+ }
1105
+ if (API_KEY) {
1106
+ console.log(`✓ API key authentication enabled (${API_KEY.slice(0, 8)}...)`);
1107
+ }
1108
+ });
1109
+
1110
+ // Graceful shutdown
1111
+ process.on('SIGINT', () => {
1112
+ console.log('\n\nShutting down gracefully...');
1113
+ httpServer.close(() => {
1114
+ console.log('Server closed');
1115
+ process.exit(0);
1116
+ });
1117
+ });
1118
+ }
1119
+
1120
+ startup();