@lydt/geogebra-studio 0.0.0-stage → 0.4.0-rc.2

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 (189) hide show
  1. package/.agents/plugins/marketplace.json +21 -0
  2. package/.codex-plugin/plugin.json +21 -0
  3. package/.mcp.json +18 -0
  4. package/LICENSE +339 -0
  5. package/LICENSE-MIT +21 -0
  6. package/README.md +108 -2
  7. package/README.zh-CN.md +89 -0
  8. package/THIRD_PARTY_NOTICES.md +35 -0
  9. package/dist/cli.d.ts +27 -0
  10. package/dist/cli.d.ts.map +1 -0
  11. package/dist/cli.js +234 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/export/files.d.ts +4 -0
  14. package/dist/export/files.d.ts.map +1 -0
  15. package/dist/export/files.js +44 -0
  16. package/dist/export/files.js.map +1 -0
  17. package/dist/export/gikz.d.ts +11 -0
  18. package/dist/export/gikz.d.ts.map +1 -0
  19. package/dist/export/gikz.js +28 -0
  20. package/dist/export/gikz.js.map +1 -0
  21. package/dist/export/html.d.ts +14 -0
  22. package/dist/export/html.d.ts.map +1 -0
  23. package/dist/export/html.js +97 -0
  24. package/dist/export/html.js.map +1 -0
  25. package/dist/export/semantic.d.ts +43 -0
  26. package/dist/export/semantic.d.ts.map +1 -0
  27. package/dist/export/semantic.js +212 -0
  28. package/dist/export/semantic.js.map +1 -0
  29. package/dist/export/viewer-themes.d.ts +76 -0
  30. package/dist/export/viewer-themes.d.ts.map +1 -0
  31. package/dist/export/viewer-themes.js +169 -0
  32. package/dist/export/viewer-themes.js.map +1 -0
  33. package/dist/export/workspace.d.ts +4 -0
  34. package/dist/export/workspace.d.ts.map +1 -0
  35. package/dist/export/workspace.js +25 -0
  36. package/dist/export/workspace.js.map +1 -0
  37. package/dist/gikz-browser.js +770 -0
  38. package/dist/index.d.ts +5 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +84 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/server.d.ts +35 -0
  43. package/dist/server.d.ts.map +1 -0
  44. package/dist/server.js +187 -0
  45. package/dist/server.js.map +1 -0
  46. package/dist/studio/artifact.d.ts +37 -0
  47. package/dist/studio/artifact.d.ts.map +1 -0
  48. package/dist/studio/artifact.js +61 -0
  49. package/dist/studio/artifact.js.map +1 -0
  50. package/dist/studio/browser.d.ts +7 -0
  51. package/dist/studio/browser.d.ts.map +1 -0
  52. package/dist/studio/browser.js +67 -0
  53. package/dist/studio/browser.js.map +1 -0
  54. package/dist/studio/install.d.ts +4 -0
  55. package/dist/studio/install.d.ts.map +1 -0
  56. package/dist/studio/install.js +60 -0
  57. package/dist/studio/install.js.map +1 -0
  58. package/dist/tools/apps-api-tools.d.ts +4 -0
  59. package/dist/tools/apps-api-tools.d.ts.map +1 -0
  60. package/dist/tools/apps-api-tools.js +96 -0
  61. package/dist/tools/apps-api-tools.js.map +1 -0
  62. package/dist/tools/educational-templates.d.ts +33 -0
  63. package/dist/tools/educational-templates.d.ts.map +1 -0
  64. package/dist/tools/educational-templates.js +261 -0
  65. package/dist/tools/educational-templates.js.map +1 -0
  66. package/dist/tools/export-tools.d.ts +3 -0
  67. package/dist/tools/export-tools.d.ts.map +1 -0
  68. package/dist/tools/export-tools.js +118 -0
  69. package/dist/tools/export-tools.js.map +1 -0
  70. package/dist/tools/geogebra-tools.d.ts +15 -0
  71. package/dist/tools/geogebra-tools.d.ts.map +1 -0
  72. package/dist/tools/geogebra-tools.js +3442 -0
  73. package/dist/tools/geogebra-tools.js.map +1 -0
  74. package/dist/tools/index.d.ts +43 -0
  75. package/dist/tools/index.d.ts.map +1 -0
  76. package/dist/tools/index.js +183 -0
  77. package/dist/tools/index.js.map +1 -0
  78. package/dist/tools/performance-tools.d.ts +7 -0
  79. package/dist/tools/performance-tools.d.ts.map +1 -0
  80. package/dist/tools/performance-tools.js +312 -0
  81. package/dist/tools/performance-tools.js.map +1 -0
  82. package/dist/tools/templates/algebra-templates.d.ts +7 -0
  83. package/dist/tools/templates/algebra-templates.d.ts.map +1 -0
  84. package/dist/tools/templates/algebra-templates.js +568 -0
  85. package/dist/tools/templates/algebra-templates.js.map +1 -0
  86. package/dist/tools/templates/geometry-templates.d.ts +7 -0
  87. package/dist/tools/templates/geometry-templates.d.ts.map +1 -0
  88. package/dist/tools/templates/geometry-templates.js +492 -0
  89. package/dist/tools/templates/geometry-templates.js.map +1 -0
  90. package/dist/types/geogebra.d.ts +103 -0
  91. package/dist/types/geogebra.d.ts.map +1 -0
  92. package/dist/types/geogebra.js +34 -0
  93. package/dist/types/geogebra.js.map +1 -0
  94. package/dist/types/mcp.d.ts +125 -0
  95. package/dist/types/mcp.d.ts.map +1 -0
  96. package/dist/types/mcp.js +19 -0
  97. package/dist/types/mcp.js.map +1 -0
  98. package/dist/utils/app-info.d.ts +12 -0
  99. package/dist/utils/app-info.d.ts.map +1 -0
  100. package/dist/utils/app-info.js +30 -0
  101. package/dist/utils/app-info.js.map +1 -0
  102. package/dist/utils/errors.d.ts +37 -0
  103. package/dist/utils/errors.d.ts.map +1 -0
  104. package/dist/utils/errors.js +72 -0
  105. package/dist/utils/errors.js.map +1 -0
  106. package/dist/utils/gemini-compatibility.d.ts +39 -0
  107. package/dist/utils/gemini-compatibility.d.ts.map +1 -0
  108. package/dist/utils/gemini-compatibility.js +154 -0
  109. package/dist/utils/gemini-compatibility.js.map +1 -0
  110. package/dist/utils/geogebra-instance.d.ts +252 -0
  111. package/dist/utils/geogebra-instance.d.ts.map +1 -0
  112. package/dist/utils/geogebra-instance.js +1560 -0
  113. package/dist/utils/geogebra-instance.js.map +1 -0
  114. package/dist/utils/geogebra-mock.d.ts +172 -0
  115. package/dist/utils/geogebra-mock.d.ts.map +1 -0
  116. package/dist/utils/geogebra-mock.js +732 -0
  117. package/dist/utils/geogebra-mock.js.map +1 -0
  118. package/dist/utils/logger.d.ts +4 -0
  119. package/dist/utils/logger.d.ts.map +1 -0
  120. package/dist/utils/logger.js +46 -0
  121. package/dist/utils/logger.js.map +1 -0
  122. package/dist/utils/performance/index.d.ts +76 -0
  123. package/dist/utils/performance/index.d.ts.map +1 -0
  124. package/dist/utils/performance/index.js +163 -0
  125. package/dist/utils/performance/index.js.map +1 -0
  126. package/dist/utils/performance/instance-pool.d.ts +78 -0
  127. package/dist/utils/performance/instance-pool.d.ts.map +1 -0
  128. package/dist/utils/performance/instance-pool.js +268 -0
  129. package/dist/utils/performance/instance-pool.js.map +1 -0
  130. package/dist/utils/validation.d.ts +92 -0
  131. package/dist/utils/validation.d.ts.map +1 -0
  132. package/dist/utils/validation.js +557 -0
  133. package/dist/utils/validation.js.map +1 -0
  134. package/dist/widget.html +110 -0
  135. package/docs/APPS-API-BRIDGE.md +28 -0
  136. package/docs/MODERNIZATION.md +15 -0
  137. package/docs/TANGENT-REGRESSION.md +34 -0
  138. package/docs/VALIDATION.md +26 -0
  139. package/docs/VISUAL-STANDARD.md +26 -0
  140. package/docs/images/native-construction.png +0 -0
  141. package/examples/tangent-construction.json +26 -0
  142. package/gikz/LICENSE +339 -0
  143. package/gikz/PROVENANCE.md +8 -0
  144. package/gikz/browser.cjs +2 -0
  145. package/gikz/cli.cjs +195 -0
  146. package/gikz/core.cjs +806 -0
  147. package/mcp.json +20 -0
  148. package/package.json +107 -4
  149. package/plugin.json +35 -0
  150. package/runtime/package.json +4 -0
  151. package/scripts/build-plugin.cjs +15 -0
  152. package/scripts/build-widget.cjs +7 -0
  153. package/skills/geogebra-construct/SKILL.md +40 -0
  154. package/skills/geogebra-setup/SKILL.md +37 -0
  155. package/skills/geometry-export/SKILL.md +31 -0
  156. package/src/cli.ts +212 -0
  157. package/src/export/files.ts +26 -0
  158. package/src/export/gikz.ts +22 -0
  159. package/src/export/html.ts +108 -0
  160. package/src/export/semantic.ts +180 -0
  161. package/src/export/viewer-themes.ts +165 -0
  162. package/src/export/workspace.ts +15 -0
  163. package/src/index.ts +71 -0
  164. package/src/server.ts +168 -0
  165. package/src/studio/artifact.ts +50 -0
  166. package/src/studio/browser.ts +38 -0
  167. package/src/studio/install.ts +43 -0
  168. package/src/tools/apps-api-tools.ts +96 -0
  169. package/src/tools/educational-templates.ts +299 -0
  170. package/src/tools/export-tools.ts +110 -0
  171. package/src/tools/geogebra-tools.ts +3836 -0
  172. package/src/tools/index.ts +192 -0
  173. package/src/tools/performance-tools.ts +322 -0
  174. package/src/tools/templates/algebra-templates.ts +619 -0
  175. package/src/tools/templates/geometry-templates.ts +550 -0
  176. package/src/types/geogebra.ts +140 -0
  177. package/src/types/global.d.ts +31 -0
  178. package/src/types/mcp.ts +141 -0
  179. package/src/utils/app-info.ts +35 -0
  180. package/src/utils/errors.ts +97 -0
  181. package/src/utils/gemini-compatibility.ts +193 -0
  182. package/src/utils/geogebra-instance.ts +1712 -0
  183. package/src/utils/geogebra-mock.ts +867 -0
  184. package/src/utils/logger.ts +60 -0
  185. package/src/utils/performance/index.ts +211 -0
  186. package/src/utils/performance/instance-pool.ts +323 -0
  187. package/src/utils/validation.ts +647 -0
  188. package/tsconfig.json +35 -0
  189. package/widget/main.ts +12 -0
@@ -0,0 +1,647 @@
1
+ /**
2
+ * Validation utilities for GeoGebra MCP tools
3
+ */
4
+
5
+ export interface ValidationResult {
6
+ isValid: boolean;
7
+ error?: string;
8
+ }
9
+
10
+ /**
11
+ * Validate point name (GeoGebra naming conventions)
12
+ */
13
+ export function validatePointName(name: string): ValidationResult {
14
+ if (!name || typeof name !== 'string') {
15
+ return { isValid: false, error: 'Point name must be a non-empty string' };
16
+ }
17
+
18
+ if (name.length === 0) {
19
+ return { isValid: false, error: 'Point name cannot be empty' };
20
+ }
21
+
22
+ // GeoGebra naming rules: start with letter, can contain letters, numbers, underscore
23
+ const namePattern = /^[a-zA-Z][a-zA-Z0-9_]*$/;
24
+ if (!namePattern.test(name)) {
25
+ return { isValid: false, error: 'Point name must start with a letter and contain only letters, numbers, and underscores' };
26
+ }
27
+
28
+ return { isValid: true };
29
+ }
30
+
31
+ /**
32
+ * Validate object name (general GeoGebra naming conventions)
33
+ */
34
+ export function validateObjectName(name: string): ValidationResult {
35
+ return validatePointName(name); // Same rules apply
36
+ }
37
+
38
+ /**
39
+ * Validate coordinates
40
+ */
41
+ export function validateCoordinates(x: number, y: number): ValidationResult {
42
+ if (typeof x !== 'number' || typeof y !== 'number') {
43
+ return { isValid: false, error: 'Coordinates must be numbers' };
44
+ }
45
+
46
+ if (!isFinite(x) || !isFinite(y)) {
47
+ return { isValid: false, error: 'Coordinates must be finite numbers' };
48
+ }
49
+
50
+ // Reasonable coordinate range for visualization
51
+ const maxCoord = 1000000;
52
+ if (Math.abs(x) > maxCoord || Math.abs(y) > maxCoord) {
53
+ return { isValid: false, error: `Coordinates must be within range [-${maxCoord}, ${maxCoord}]` };
54
+ }
55
+
56
+ return { isValid: true };
57
+ }
58
+
59
+ /**
60
+ * Validate radius
61
+ */
62
+ export function validateRadius(radius: number): ValidationResult {
63
+ if (typeof radius !== 'number') {
64
+ return { isValid: false, error: 'Radius must be a number' };
65
+ }
66
+
67
+ if (!isFinite(radius)) {
68
+ return { isValid: false, error: 'Radius must be a finite number' };
69
+ }
70
+
71
+ if (radius < 0) {
72
+ return { isValid: false, error: 'Radius must be non-negative' };
73
+ }
74
+
75
+ if (radius > 1000000) {
76
+ return { isValid: false, error: 'Radius must be reasonable (≤ 1,000,000)' };
77
+ }
78
+
79
+ return { isValid: true };
80
+ }
81
+
82
+ /**
83
+ * Validate polygon vertices
84
+ */
85
+ export function validatePolygonVertices(vertices: string[]): ValidationResult {
86
+ if (!Array.isArray(vertices)) {
87
+ return { isValid: false, error: 'Vertices must be an array' };
88
+ }
89
+
90
+ if (vertices.length < 3) {
91
+ return { isValid: false, error: 'Polygon must have at least 3 vertices' };
92
+ }
93
+
94
+ if (vertices.length > 100) {
95
+ return { isValid: false, error: 'Polygon cannot have more than 100 vertices' };
96
+ }
97
+
98
+ // Check for duplicate vertices
99
+ const uniqueVertices = new Set(vertices);
100
+ if (uniqueVertices.size !== vertices.length) {
101
+ return { isValid: false, error: 'Polygon vertices must be unique' };
102
+ }
103
+
104
+ // Validate each vertex name
105
+ for (let i = 0; i < vertices.length; i++) {
106
+ const vertex = vertices[i];
107
+ if (!vertex) {
108
+ return { isValid: false, error: `Vertex ${i + 1} is undefined or empty` };
109
+ }
110
+ const validation = validatePointName(vertex);
111
+ if (!validation.isValid) {
112
+ return { isValid: false, error: `Vertex ${i + 1}: ${validation.error}` };
113
+ }
114
+ }
115
+
116
+ return { isValid: true };
117
+ }
118
+
119
+ /**
120
+ * Validate linear equation format
121
+ */
122
+ export function validateLinearEquation(equation: string): ValidationResult {
123
+ if (!equation || typeof equation !== 'string') {
124
+ return { isValid: false, error: 'Equation must be a non-empty string' };
125
+ }
126
+
127
+ if (!equation.includes('=')) {
128
+ return { isValid: false, error: 'Equation must contain an equals sign (=)' };
129
+ }
130
+
131
+ // Basic validation for common linear equation formats
132
+ const patterns = [
133
+ /^y\s*=\s*[+-]?\s*\d*\.?\d*\s*\*?\s*x\s*[+-]\s*\d+\.?\d*$/, // y = mx + b
134
+ /^y\s*=\s*[+-]?\s*\d+\.?\d*\s*\*?\s*x$/, // y = mx
135
+ /^y\s*=\s*[+-]?\s*\d+\.?\d*$/, // y = b (horizontal line)
136
+ /^x\s*=\s*[+-]?\s*\d+\.?\d*$/, // x = a (vertical line)
137
+ /^[+-]?\s*\d*\.?\d*\s*\*?\s*x\s*[+-]\s*\d*\.?\d*\s*\*?\s*y\s*=\s*[+-]?\s*\d+\.?\d*$/, // ax + by = c
138
+ ];
139
+
140
+ const normalizedEq = equation.replace(/\s+/g, ' ').trim();
141
+ const isValidFormat = patterns.some(pattern => pattern.test(normalizedEq));
142
+
143
+ if (!isValidFormat) {
144
+ return {
145
+ isValid: false,
146
+ error: 'Equation format not recognized. Use formats like "y = 2x + 3", "x = 5", or "2x + 3y = 6"'
147
+ };
148
+ }
149
+
150
+ return { isValid: true };
151
+ }
152
+
153
+ /**
154
+ * Validate that required parameters are provided for a given method
155
+ */
156
+ export function validateRequiredParams(params: Record<string, unknown>, required: string[]): ValidationResult {
157
+ for (const param of required) {
158
+ if (!(param in params) || params[param] === undefined || params[param] === null) {
159
+ return { isValid: false, error: `Missing required parameter: ${param}` };
160
+ }
161
+ }
162
+ return { isValid: true };
163
+ }
164
+
165
+ /**
166
+ * Validate function expression for standard functions f(x) = ...
167
+ */
168
+ export function validateFunctionExpression(expression: string): ValidationResult {
169
+ if (!expression || typeof expression !== 'string') {
170
+ return { isValid: false, error: 'Function expression must be a non-empty string' };
171
+ }
172
+
173
+ const trimmed = expression.trim();
174
+ if (trimmed.length === 0) {
175
+ return { isValid: false, error: 'Function expression cannot be empty' };
176
+ }
177
+
178
+ // Basic validation for function expressions
179
+ // Allow common mathematical functions and operators
180
+ const allowedPattern = /^[x\d\+\-\*\/\^\(\)\.\s,sincotaglnbsqrtepie]*$/i;
181
+ if (!allowedPattern.test(trimmed)) {
182
+ return { isValid: false, error: 'Function expression contains invalid characters. Use only x, numbers, +, -, *, /, ^, (), sin, cos, tan, log, ln, sqrt, abs, e, pi' };
183
+ }
184
+
185
+ // Check for balanced parentheses
186
+ let parenCount = 0;
187
+ for (const char of trimmed) {
188
+ if (char === '(') parenCount++;
189
+ if (char === ')') parenCount--;
190
+ if (parenCount < 0) {
191
+ return { isValid: false, error: 'Unbalanced parentheses in function expression' };
192
+ }
193
+ }
194
+ if (parenCount !== 0) {
195
+ return { isValid: false, error: 'Unbalanced parentheses in function expression' };
196
+ }
197
+
198
+ return { isValid: true };
199
+ }
200
+
201
+ /**
202
+ * Validate parametric function expressions
203
+ */
204
+ export function validateParametricExpressions(xExpr: string, yExpr: string, parameter: string): ValidationResult {
205
+ if (!parameter || typeof parameter !== 'string') {
206
+ return { isValid: false, error: 'Parameter name must be a non-empty string' };
207
+ }
208
+
209
+ // Validate parameter name (typically t, u, s, etc.)
210
+ const paramPattern = /^[a-zA-Z][a-zA-Z0-9]*$/;
211
+ if (!paramPattern.test(parameter)) {
212
+ return { isValid: false, error: 'Parameter name must start with a letter and contain only letters and numbers' };
213
+ }
214
+
215
+ // Validate x expression
216
+ const xValidation = validateParametricExpression(xExpr, parameter);
217
+ if (!xValidation.isValid) {
218
+ return { isValid: false, error: `X expression: ${xValidation.error}` };
219
+ }
220
+
221
+ // Validate y expression
222
+ const yValidation = validateParametricExpression(yExpr, parameter);
223
+ if (!yValidation.isValid) {
224
+ return { isValid: false, error: `Y expression: ${yValidation.error}` };
225
+ }
226
+
227
+ return { isValid: true };
228
+ }
229
+
230
+ /**
231
+ * Validate a single parametric expression
232
+ */
233
+ function validateParametricExpression(expression: string, parameter: string): ValidationResult {
234
+ if (!expression || typeof expression !== 'string') {
235
+ return { isValid: false, error: 'Expression must be a non-empty string' };
236
+ }
237
+
238
+ const trimmed = expression.trim();
239
+ if (trimmed.length === 0) {
240
+ return { isValid: false, error: 'Expression cannot be empty' };
241
+ }
242
+
243
+ // Allow the parameter variable and common mathematical functions
244
+ const escapedParam = parameter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
245
+ const allowedPattern = new RegExp(`^[${escapedParam}\\d\\+\\-\\*\\/\\^\\(\\)\\.\\s,sincotaglnbsqrtepie]*$`, 'i');
246
+
247
+ if (!allowedPattern.test(trimmed)) {
248
+ return { isValid: false, error: `Expression contains invalid characters. Use only ${parameter}, numbers, +, -, *, /, ^, (), sin, cos, tan, log, ln, sqrt, abs, e, pi` };
249
+ }
250
+
251
+ // Check for balanced parentheses
252
+ let parenCount = 0;
253
+ for (const char of trimmed) {
254
+ if (char === '(') parenCount++;
255
+ if (char === ')') parenCount--;
256
+ if (parenCount < 0) {
257
+ return { isValid: false, error: 'Unbalanced parentheses in expression' };
258
+ }
259
+ }
260
+ if (parenCount !== 0) {
261
+ return { isValid: false, error: 'Unbalanced parentheses in expression' };
262
+ }
263
+
264
+ return { isValid: true };
265
+ }
266
+
267
+ /**
268
+ * Validate implicit function expression
269
+ */
270
+ export function validateImplicitExpression(expression: string): ValidationResult {
271
+ if (!expression || typeof expression !== 'string') {
272
+ return { isValid: false, error: 'Implicit expression must be a non-empty string' };
273
+ }
274
+
275
+ const trimmed = expression.trim();
276
+ if (trimmed.length === 0) {
277
+ return { isValid: false, error: 'Implicit expression cannot be empty' };
278
+ }
279
+
280
+ // Must contain both x and y variables
281
+ if (!trimmed.includes('x') || !trimmed.includes('y')) {
282
+ return { isValid: false, error: 'Implicit expression must contain both x and y variables' };
283
+ }
284
+
285
+ // Allow x, y, numbers, and mathematical functions
286
+ const allowedPattern = /^[xy\d\+\-\*\/\^\(\)\.\s,sincotaglnbsqrtepie=]*$/i;
287
+ if (!allowedPattern.test(trimmed)) {
288
+ return { isValid: false, error: 'Implicit expression contains invalid characters. Use only x, y, numbers, +, -, *, /, ^, (), sin, cos, tan, log, ln, sqrt, abs, e, pi, =' };
289
+ }
290
+
291
+ // Check for balanced parentheses
292
+ let parenCount = 0;
293
+ for (const char of trimmed) {
294
+ if (char === '(') parenCount++;
295
+ if (char === ')') parenCount--;
296
+ if (parenCount < 0) {
297
+ return { isValid: false, error: 'Unbalanced parentheses in implicit expression' };
298
+ }
299
+ }
300
+ if (parenCount !== 0) {
301
+ return { isValid: false, error: 'Unbalanced parentheses in implicit expression' };
302
+ }
303
+
304
+ return { isValid: true };
305
+ }
306
+
307
+ /**
308
+ * Validate domain range parameters
309
+ */
310
+ export function validateDomainRange(min: number, max: number, paramName: string = 'parameter'): ValidationResult {
311
+ if (typeof min !== 'number' || typeof max !== 'number') {
312
+ return { isValid: false, error: `${paramName} range bounds must be numbers` };
313
+ }
314
+
315
+ if (!isFinite(min) || !isFinite(max)) {
316
+ return { isValid: false, error: `${paramName} range bounds must be finite numbers` };
317
+ }
318
+
319
+ if (min >= max) {
320
+ return { isValid: false, error: `${paramName} minimum must be less than maximum` };
321
+ }
322
+
323
+ // Reasonable range limits
324
+ const maxRange = 1000000;
325
+ if (Math.abs(min) > maxRange || Math.abs(max) > maxRange) {
326
+ return { isValid: false, error: `${paramName} range must be within [-${maxRange}, ${maxRange}]` };
327
+ }
328
+
329
+ const rangeDiff = max - min;
330
+ if (rangeDiff < 0.001) {
331
+ return { isValid: false, error: `${paramName} range must be at least 0.001 units wide` };
332
+ }
333
+
334
+ if (rangeDiff > maxRange * 2) {
335
+ return { isValid: false, error: `${paramName} range cannot exceed ${maxRange * 2} units wide` };
336
+ }
337
+
338
+ return { isValid: true };
339
+ }
340
+
341
+ /**
342
+ * Validate function styling parameters
343
+ */
344
+ export function validateFunctionStyling(color?: string, thickness?: number, style?: string): ValidationResult {
345
+ if (color !== undefined) {
346
+ if (typeof color !== 'string') {
347
+ return { isValid: false, error: 'Color must be a string' };
348
+ }
349
+
350
+ // Basic color validation (hex, named colors, rgb)
351
+ const colorPattern = /^(#[0-9A-Fa-f]{6}|#[0-9A-Fa-f]{3}|rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)|[a-zA-Z]+)$/;
352
+ if (!colorPattern.test(color.trim())) {
353
+ return { isValid: false, error: 'Color must be a valid hex color (#RRGGBB), RGB value (rgb(r,g,b)), or color name' };
354
+ }
355
+ }
356
+
357
+ if (thickness !== undefined) {
358
+ if (typeof thickness !== 'number') {
359
+ return { isValid: false, error: 'Line thickness must be a number' };
360
+ }
361
+
362
+ if (!isFinite(thickness)) {
363
+ return { isValid: false, error: 'Line thickness must be a finite number' };
364
+ }
365
+
366
+ if (thickness < 1 || thickness > 10) {
367
+ return { isValid: false, error: 'Line thickness must be between 1 and 10' };
368
+ }
369
+ }
370
+
371
+ if (style !== undefined) {
372
+ if (typeof style !== 'string') {
373
+ return { isValid: false, error: 'Line style must be a string' };
374
+ }
375
+
376
+ const validStyles = ['solid', 'dashed', 'dotted'];
377
+ if (!validStyles.includes(style.toLowerCase())) {
378
+ return { isValid: false, error: `Line style must be one of: ${validStyles.join(', ')}` };
379
+ }
380
+ }
381
+
382
+ return { isValid: true };
383
+ }
384
+
385
+ /**
386
+ * Validate algebraic expression for CAS operations
387
+ */
388
+ export function validateAlgebraicExpression(expression: string): ValidationResult {
389
+ if (!expression || typeof expression !== 'string') {
390
+ return { isValid: false, error: 'Expression must be a non-empty string' };
391
+ }
392
+
393
+ const trimmed = expression.trim();
394
+ if (trimmed.length === 0) {
395
+ return { isValid: false, error: 'Expression cannot be empty' };
396
+ }
397
+
398
+ // Allow common mathematical symbols, variables, and functions
399
+ const allowedPattern = /^[a-zA-Z\d\+\-\*\/\^\(\)\.\s,=]*$/;
400
+ if (!allowedPattern.test(trimmed)) {
401
+ return {
402
+ isValid: false,
403
+ error: 'Expression contains invalid characters. Use only variables, numbers, +, -, *, /, ^, =, ()'
404
+ };
405
+ }
406
+
407
+ // Check for balanced parentheses
408
+ let parenCount = 0;
409
+ for (const char of trimmed) {
410
+ if (char === '(') parenCount++;
411
+ if (char === ')') parenCount--;
412
+ if (parenCount < 0) {
413
+ return { isValid: false, error: 'Unbalanced parentheses in expression' };
414
+ }
415
+ }
416
+ if (parenCount !== 0) {
417
+ return { isValid: false, error: 'Unbalanced parentheses in expression' };
418
+ }
419
+
420
+ return { isValid: true };
421
+ }
422
+
423
+ /**
424
+ * Validate equation format for solving
425
+ */
426
+ export function validateEquation(equation: string): ValidationResult {
427
+ if (!equation || typeof equation !== 'string') {
428
+ return { isValid: false, error: 'Equation must be a non-empty string' };
429
+ }
430
+
431
+ const trimmed = equation.trim();
432
+ if (trimmed.length === 0) {
433
+ return { isValid: false, error: 'Equation cannot be empty' };
434
+ }
435
+
436
+ // Check if equation contains exactly one equals sign
437
+ const equalsSigns = (trimmed.match(/=/g) || []).length;
438
+ if (equalsSigns === 0) {
439
+ return { isValid: false, error: 'Equation must contain an equals sign (=)' };
440
+ }
441
+ if (equalsSigns > 1) {
442
+ return { isValid: false, error: 'Equation cannot contain multiple equals signs' };
443
+ }
444
+
445
+ // Validate both sides of the equation
446
+ const parts = trimmed.split('=');
447
+ const leftSide = parts[0]?.trim() || '';
448
+ const rightSide = parts[1]?.trim() || '';
449
+
450
+ const leftValidation = validateAlgebraicExpression(leftSide);
451
+ if (!leftValidation.isValid) {
452
+ return { isValid: false, error: `Left side of equation: ${leftValidation.error}` };
453
+ }
454
+
455
+ const rightValidation = validateAlgebraicExpression(rightSide);
456
+ if (!rightValidation.isValid) {
457
+ return { isValid: false, error: `Right side of equation: ${rightValidation.error}` };
458
+ }
459
+
460
+ return { isValid: true };
461
+ }
462
+
463
+ /**
464
+ * Validate variable name for CAS operations
465
+ */
466
+ export function validateVariableName(variable: string): ValidationResult {
467
+ if (!variable || typeof variable !== 'string') {
468
+ return { isValid: false, error: 'Variable name must be a non-empty string' };
469
+ }
470
+
471
+ const trimmed = variable.trim();
472
+ if (trimmed.length === 0) {
473
+ return { isValid: false, error: 'Variable name cannot be empty' };
474
+ }
475
+
476
+ // Variable must be a single letter or letter followed by digits/underscores
477
+ const variablePattern = /^[a-zA-Z][a-zA-Z0-9_]*$/;
478
+ if (!variablePattern.test(trimmed)) {
479
+ return {
480
+ isValid: false,
481
+ error: 'Variable name must start with a letter and contain only letters, numbers, and underscores'
482
+ };
483
+ }
484
+
485
+ // Check for reserved function names
486
+ const reservedNames = ['sin', 'cos', 'tan', 'log', 'ln', 'abs', 'sqrt', 'exp', 'pi', 'e'];
487
+ if (reservedNames.includes(trimmed.toLowerCase())) {
488
+ return { isValid: false, error: `"${trimmed}" is a reserved function name and cannot be used as a variable` };
489
+ }
490
+
491
+ return { isValid: true };
492
+ }
493
+
494
+ /**
495
+ * Validate system of equations
496
+ */
497
+ export function validateSystemOfEquations(equations: string[]): ValidationResult {
498
+ if (!Array.isArray(equations)) {
499
+ return { isValid: false, error: 'Equations must be provided as an array' };
500
+ }
501
+
502
+ if (equations.length === 0) {
503
+ return { isValid: false, error: 'At least one equation must be provided' };
504
+ }
505
+
506
+ if (equations.length > 10) {
507
+ return { isValid: false, error: 'Cannot solve more than 10 equations simultaneously' };
508
+ }
509
+
510
+ // Validate each equation
511
+ for (let i = 0; i < equations.length; i++) {
512
+ const equation = equations[i];
513
+ if (equation === undefined || equation === null) {
514
+ return { isValid: false, error: `Equation ${i + 1} is undefined or null` };
515
+ }
516
+ const validation = validateEquation(equation);
517
+ if (!validation.isValid) {
518
+ return { isValid: false, error: `Equation ${i + 1}: ${validation.error}` };
519
+ }
520
+ }
521
+
522
+ return { isValid: true };
523
+ }
524
+
525
+ /**
526
+ * Validate variables list for system solving
527
+ */
528
+ export function validateVariablesList(variables: string[]): ValidationResult {
529
+ if (!Array.isArray(variables)) {
530
+ return { isValid: false, error: 'Variables must be provided as an array' };
531
+ }
532
+
533
+ if (variables.length === 0) {
534
+ return { isValid: false, error: 'At least one variable must be specified' };
535
+ }
536
+
537
+ if (variables.length > 10) {
538
+ return { isValid: false, error: 'Cannot solve for more than 10 variables simultaneously' };
539
+ }
540
+
541
+ // Validate each variable name
542
+ for (let i = 0; i < variables.length; i++) {
543
+ const variable = variables[i];
544
+ if (variable === undefined || variable === null) {
545
+ return { isValid: false, error: `Variable ${i + 1} is undefined or null` };
546
+ }
547
+ const validation = validateVariableName(variable);
548
+ if (!validation.isValid) {
549
+ return { isValid: false, error: `Variable ${i + 1}: ${validation.error}` };
550
+ }
551
+ }
552
+
553
+ // Check for duplicate variables
554
+ const uniqueVars = new Set(variables);
555
+ if (uniqueVars.size !== variables.length) {
556
+ return { isValid: false, error: 'Variables list cannot contain duplicates' };
557
+ }
558
+
559
+ return { isValid: true };
560
+ }
561
+
562
+ /**
563
+ * Validates slider parameters for creation
564
+ */
565
+ export function validateSliderParameters(
566
+ name: string,
567
+ min: number,
568
+ max: number,
569
+ increment?: number,
570
+ defaultValue?: number
571
+ ): ValidationResult {
572
+ // Validate name
573
+ const nameValidation = validateObjectName(name);
574
+ if (!nameValidation.isValid) {
575
+ return nameValidation;
576
+ }
577
+
578
+ // Validate range
579
+ if (min >= max) {
580
+ return { isValid: false, error: 'Minimum value must be less than maximum value' };
581
+ }
582
+
583
+ // Validate increment
584
+ if (increment !== undefined && increment <= 0) {
585
+ return { isValid: false, error: 'Increment must be positive' };
586
+ }
587
+
588
+ // Validate default value
589
+ if (defaultValue !== undefined && (defaultValue < min || defaultValue > max)) {
590
+ return { isValid: false, error: 'Default value must be within the specified range' };
591
+ }
592
+
593
+ return { isValid: true };
594
+ }
595
+
596
+ /**
597
+ * Validates animation speed parameter
598
+ */
599
+ export function validateAnimationSpeed(speed: number): ValidationResult {
600
+ if (speed <= 0) {
601
+ return { isValid: false, error: 'Animation speed must be positive' };
602
+ }
603
+
604
+ if (speed > 10) {
605
+ return { isValid: false, error: 'Animation speed should not exceed 10 for reasonable performance' };
606
+ }
607
+
608
+ return { isValid: true };
609
+ }
610
+
611
+ /**
612
+ * Validates animation direction parameter
613
+ */
614
+ export function validateAnimationDirection(direction?: string): ValidationResult {
615
+ if (direction && !['forward', 'backward', 'oscillating'].includes(direction)) {
616
+ return { isValid: false, error: 'Animation direction must be "forward", "backward", or "oscillating"' };
617
+ }
618
+
619
+ return { isValid: true };
620
+ }
621
+
622
+ /**
623
+ * Validates parameters for animation export
624
+ */
625
+ export function validateAnimationExportParameters(
626
+ frameCount: number,
627
+ frameDelay: number,
628
+ totalDuration?: number
629
+ ): ValidationResult {
630
+ if (frameCount <= 0 || !Number.isInteger(frameCount)) {
631
+ return { isValid: false, error: 'Frame count must be a positive integer' };
632
+ }
633
+
634
+ if (frameCount > 300) {
635
+ return { isValid: false, error: 'Frame count should not exceed 300 for reasonable file size' };
636
+ }
637
+
638
+ if (frameDelay <= 0) {
639
+ return { isValid: false, error: 'Frame delay must be positive' };
640
+ }
641
+
642
+ if (totalDuration !== undefined && totalDuration <= 0) {
643
+ return { isValid: false, error: 'Total duration must be positive' };
644
+ }
645
+
646
+ return { isValid: true };
647
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "commonjs",
5
+ "lib": ["ES2022"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "removeComments": false,
17
+ "noImplicitAny": true,
18
+ "noImplicitReturns": true,
19
+ "noImplicitThis": true,
20
+ "noUnusedLocals": true,
21
+ "noUnusedParameters": true,
22
+ "exactOptionalPropertyTypes": true,
23
+ "noImplicitOverride": true,
24
+ "noPropertyAccessFromIndexSignature": true,
25
+ "noUncheckedIndexedAccess": true
26
+ },
27
+ "include": [
28
+ "src/**/*"
29
+ ],
30
+ "exclude": [
31
+ "node_modules",
32
+ "dist",
33
+ "tests"
34
+ ]
35
+ }
package/widget/main.ts ADDED
@@ -0,0 +1,12 @@
1
+ declare const __STUDIO_VERSION__:string;
2
+ import { App } from '@modelcontextprotocol/ext-apps';
3
+ const app = new App({name:'GeoGebra construction preview',version:__STUDIO_VERSION__},{});
4
+ const frame = document.querySelector<HTMLIFrameElement>('#preview')!;
5
+ const status = document.querySelector<HTMLElement>('#status')!;
6
+ app.addEventListener('toolresult', result => {
7
+ const data=result.structuredContent as {html?:string;error?:string}|undefined;
8
+ if(result.isError || !data?.html){status.textContent=data?.error||'No construction HTML returned.';return;}
9
+ frame.srcdoc=data.html;
10
+ status.textContent='Interactive GeoGebra preview. The applet requires internet access.';
11
+ });
12
+ app.connect().catch(e=>{status.textContent='Host connection failed: '+String(e);});