@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
package/src/server.ts ADDED
@@ -0,0 +1,168 @@
1
+ import { EventEmitter } from 'events';
2
+ import { readFileSync } from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { inWorkspace } from './export/workspace';
6
+ import { createServer, Server as HttpServer } from 'http';
7
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
8
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
10
+ import {
11
+ CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema,
12
+ ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema,
13
+ ErrorCode, McpError,
14
+ } from '@modelcontextprotocol/sdk/types.js';
15
+ import Ajv from 'ajv';
16
+ import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
17
+ import { toolRegistry } from './tools';
18
+ import { McpServerConfig } from './types/mcp';
19
+ import { instancePool } from './tools/geogebra-tools';
20
+ import { optimizedInstancePool } from './utils/performance/instance-pool';
21
+
22
+ const guidance = 'Use geogebra_build_artifact by default to produce a checked local HTML with a packed editable GGB and preview. geogebra_create_html is an unverified command-template alternative. ' +
23
+ 'When saving files, pass workspace as the absolute user project directory; never use the installed plugin cache as the output directory. MCP roots are used when available. ' +
24
+ 'For server-side work use geogebra_eval_commands, then native exports. ' +
25
+ 'All TikZ exports use native PGF followed by Gikz; never return raw PGF as a final export. ' +
26
+ 'Use geogebra_get_semantic_scene before semantic export; report unsupported objects and coordinate fallbacks. ' +
27
+ 'Use geogebra_api_list_methods and geogebra_api_call for public Apps API methods not covered by focused tools. ' +
28
+ 'All server-side tools share one construction. Do not interleave independent diagrams.';
29
+
30
+ /** Official SDK owns framing, negotiation, notifications, and transport lifecycle. */
31
+ export class McpServer extends EventEmitter {
32
+ private sdk?: Server;
33
+ private http?: HttpServer;
34
+ private connections = new Set<Server>();
35
+ private running = false;
36
+ private queue: Promise<unknown> = Promise.resolve();
37
+ constructor(private config: McpServerConfig) { super(); }
38
+
39
+ createProtocolServer(): Server {
40
+ const sdk = new Server({ name: this.config.name, version: this.config.version }, {
41
+ capabilities: { tools: {}, resources: {}, prompts: {} }, instructions: guidance,
42
+ });
43
+ const ajv = new Ajv({ strict: false, allErrors: true });
44
+ const core = new Set(['geogebra_build_artifact','server_info','geogebra_eval_commands','geogebra_clear_construction',
45
+ 'geogebra_get_objects','geogebra_instance_status','geogebra_auto_zoom','geogebra_load_ggb',
46
+ 'geogebra_export_ggb','geogebra_export_png','geogebra_export_svg','geogebra_export_pdf',
47
+ 'geogebra_create_html','geogebra_export_html','geogebra_get_semantic_scene',
48
+ 'geogebra_export_source','geogebra_export_tikz','geogebra_clean_tikz','geogebra_api_list_methods','geogebra_api_call','geogebra_api_get_callback_events']);
49
+ const tools = toolRegistry.getTools().filter(t => process.env['GEOGEBRA_TOOL_PROFILE'] !== 'core' || core.has(t.name))
50
+ .map(tool=>tool.inputSchema.properties['filename']?{...tool,inputSchema:{...tool.inputSchema,properties:{...tool.inputSchema.properties,workspace:{type:'string',description:'Absolute user workspace directory for saved files. Pass this when the client does not provide MCP roots; never use the plugin cache directory.'}}}}:tool)
51
+ .sort((a,b)=>a.name.localeCompare(b.name));
52
+ const validators = new Map(tools.map(t => [t.name, ajv.compile(t.inputSchema)]));
53
+ sdk.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
54
+ sdk.setRequestHandler(CallToolRequestSchema, async request => {
55
+ const { name, arguments: args = {} } = request.params;
56
+ const validate = validators.get(name);
57
+ if (!validate) throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${name}`);
58
+ if (!validate(args)) throw new McpError(ErrorCode.InvalidParams, ajv.errorsText(validate.errors));
59
+ // Queue the entire operation, including export, to avoid cross-request mutation races.
60
+ const operation = this.queue.then(async () => {
61
+ try {
62
+ let workspace=args['workspace'];
63
+ if(args['filename']!==undefined&&!workspace&&sdk.getClientCapabilities()?.roots){
64
+ const roots=await sdk.listRoots({}, {timeout:5000});
65
+ const folders=roots.roots.filter(root=>root.uri.startsWith('file:')).map(root=>fileURLToPath(root.uri));
66
+ workspace=folders.length===1?folders[0]:folders.find(root=>typeof args['filename']==='string'&&path.isAbsolute(args['filename'])&&args['filename'].startsWith(root+path.sep));
67
+ }
68
+ if(args['filename']!==undefined&&!workspace&&process.env['GEOGEBRA_PLUGIN_RUNTIME']==='1'&&!process.env['VSCODE_WORKSPACE_FOLDER'])
69
+ throw new Error('Provide workspace as the absolute user project directory. The plugin cache is not an output workspace.');
70
+ const result = await inWorkspace(workspace,()=>toolRegistry.executeTool(name, args));
71
+ if (!result.structuredContent && result.content.length === 1 && result.content[0]?.text) {
72
+ try {
73
+ const value: unknown = JSON.parse(result.content[0].text);
74
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
75
+ return { ...result, structuredContent: value as Record<string, unknown> };
76
+ }
77
+ } catch { /* Existing plain text tools remain valid. */ }
78
+ }
79
+ return result;
80
+ } catch (error) {
81
+ return { content: [{ type: 'text' as const, text: error instanceof Error ? error.message : String(error) }], isError: true };
82
+ }
83
+ });
84
+ this.queue = operation.catch(() => undefined);
85
+ return operation;
86
+ });
87
+ sdk.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [{
88
+ uri: 'geogebra://guide', name: 'GeoGebra construction and export guide', mimeType: 'text/plain',
89
+ }, { uri: 'ui://geogebra/preview.html', name: 'GeoGebra preview', mimeType: 'text/html;profile=mcp-app' }] }));
90
+ sdk.setRequestHandler(ReadResourceRequestSchema, async request => {
91
+ if (request.params.uri === 'ui://geogebra/preview.html') return { contents: [{
92
+ uri: request.params.uri, mimeType: 'text/html;profile=mcp-app',
93
+ text: readFileSync(path.join(__dirname,'widget.html'),'utf8'),
94
+ _meta: { ui: { csp: {
95
+ resourceDomains: ['https://www.geogebra.org','https://geogebra.org','https://cdn.geogebra.org'],
96
+ connectDomains: ['https://www.geogebra.org','https://geogebra.org','https://cdn.geogebra.org'],
97
+ } } },
98
+ }] };
99
+ if (request.params.uri !== 'geogebra://guide') throw new McpError(ErrorCode.InvalidParams, 'Unknown resource');
100
+ return { contents: [{ uri: 'geogebra://guide', mimeType: 'text/plain', text: guidance }] };
101
+ });
102
+ sdk.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: [{
103
+ name: 'construct-geometry', description: 'Build an editable, print-style GeoGebra construction',
104
+ arguments: [{ name: 'problem', description: 'Geometry definitions and target exports', required: true }],
105
+ }] }));
106
+ sdk.setRequestHandler(GetPromptRequestSchema, async request => {
107
+ if (request.params.name !== 'construct-geometry' || !request.params.arguments?.['problem']) {
108
+ throw new McpError(ErrorCode.InvalidParams, 'construct-geometry requires problem');
109
+ }
110
+ return { messages: [{ role: 'user' as const, content: { type: 'text' as const,
111
+ text: `${guidance}\n\n${request.params.arguments['problem']}` } }] };
112
+ });
113
+ return sdk;
114
+ }
115
+
116
+ async start(): Promise<void> {
117
+ if (this.running) return;
118
+ this.sdk = this.createProtocolServer();
119
+ this.sdk.onclose = () => { if (this.running) void this.stop(); };
120
+ await this.sdk.connect(new StdioServerTransport());
121
+ this.running = true;
122
+ this.emit('started');
123
+ }
124
+
125
+ /** Loopback-only, single-user HTTP. Deploy behind HTTPS + authentication for remote use. */
126
+ async startHttp(port: number): Promise<void> {
127
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('Invalid HTTP port');
128
+ this.http = createServer(async (req, res) => {
129
+ if (req.url !== '/mcp') { res.writeHead(404).end(); return; }
130
+ if (req.headers.origin && ![`http://localhost:${port}`, `http://127.0.0.1:${port}`].includes(req.headers.origin)) {
131
+ res.writeHead(403).end('Origin forbidden'); return;
132
+ }
133
+ const host = req.headers.host;
134
+ if (!host || ![`localhost:${port}`, `127.0.0.1:${port}`].includes(host)) {
135
+ res.writeHead(403).end('Host forbidden'); return;
136
+ }
137
+ const sdk = this.createProtocolServer();
138
+ const transport = new StreamableHTTPServerTransport({ enableJsonResponse: true });
139
+ this.connections.add(sdk);
140
+ res.on('close', () => { void sdk.close(); this.connections.delete(sdk); });
141
+ try {
142
+ await sdk.connect(transport as Transport);
143
+ await transport.handleRequest(req, res);
144
+ } catch { if (!res.headersSent) res.writeHead(500).end('MCP request failed'); }
145
+ });
146
+ await new Promise<void>((resolve, reject) => {
147
+ this.http!.once('error', reject);
148
+ this.http!.listen(port, '127.0.0.1', resolve);
149
+ });
150
+ this.running = true;
151
+ this.emit('started');
152
+ }
153
+
154
+ async stop(): Promise<void> {
155
+ this.running = false;
156
+ await this.queue;
157
+ await this.sdk?.close();
158
+ await Promise.all([...this.connections].map(s => s.close()));
159
+ if (this.http) await new Promise<void>((resolve, reject) => this.http!.close(e => e ? reject(e) : resolve()));
160
+ await instancePool.cleanup();
161
+ await optimizedInstancePool.cleanup();
162
+ this.running = false;
163
+ this.emit('stopped');
164
+ }
165
+ getStatus() { return { ...this.config, isRunning: this.running, toolCount: toolRegistry.getToolCount(), uptime: this.running ? process.uptime() : 0 }; }
166
+ getConfig() { return { ...this.config }; }
167
+ isServerRunning() { return this.running; }
168
+ }
@@ -0,0 +1,50 @@
1
+ declare const window: any;
2
+ import { readFile, unlink } from 'fs/promises';
3
+ import { GeoGebraInstance } from '../utils/geogebra-instance';
4
+ import { constructionHtml, HtmlOptions } from '../export/html';
5
+ import { saveFile, saveText } from '../export/files';
6
+ /** Isolated native execution → packed GGB → reopen final viewer → save, never a shared pool reset. */
7
+ export async function buildArtifact(options:HtmlOptions & {filename:string}) {
8
+ if(!/\.html$/i.test(options.filename))throw new Error('filename must end in .html');
9
+ if(!options.ggbBase64&&!options.commands?.length)throw new Error('Provide nonempty commands or a packed ggbBase64');
10
+ const engine=new GeoGebraInstance({width:options.width||1000,height:options.height||800});
11
+ let filename:string|undefined;
12
+ let previewFilename:string|undefined;
13
+ let committed=false;
14
+ try{
15
+ await engine.initialize();
16
+ await engine.renderHtml(constructionHtml(options));
17
+ const checks=await engine.debugEvaluate<{objects:number;undefinedVisibleObjects:string[]}>(()=>{
18
+ const api=(window as any).ggbApplet,names=api.getAllObjectNames();
19
+ return {objects:names.length,undefinedVisibleObjects:names.filter((name:string)=>api.getVisible(name)&&!api.isDefined(name))};
20
+ });
21
+ if(checks.undefinedVisibleObjects.length)throw new Error('Undefined visible objects: '+checks.undefinedVisibleObjects.join(', '));
22
+ const ggbBase64=await engine.exportGGB();
23
+ const html=constructionHtml({...options,commands:[],ggbBase64});
24
+ filename=await saveText(options.filename,html);
25
+ if(!filename)throw new Error('Unable to save the final HTML artifact');
26
+ // Reopen the exact bytes written to disk so the preview and verification describe
27
+ // the delivered file, including its packed GGB payload.
28
+ await engine.renderHtml(await readFile(filename,'utf8'));
29
+ const layout=await engine.debugEvaluate<{ready:boolean;error:string|null;equalAxes:boolean;viewWidth:number;viewHeight:number}>(()=>{
30
+ const api=(window as any).ggbApplet,view=JSON.parse(api.getViewProperties(1));
31
+ return {ready:(window as any).ggbReady===true,error:(window as any).ggbError||null,
32
+ equalAxes:Math.abs(view.invXscale/view.invYscale-1)<1e-6,viewWidth:view.width,viewHeight:view.height};
33
+ });
34
+ if(!layout.ready||layout.error||!layout.equalAxes||layout.viewWidth<=0||layout.viewHeight<=0)
35
+ throw new Error('Final HTML viewer verification failed: '+JSON.stringify(layout));
36
+ const packedObjects=await engine.getAllObjectNames();
37
+ if(packedObjects.length!==checks.objects)throw new Error('Packed GGB round trip changed object count');
38
+ const preview=await engine.capturePreview();
39
+ previewFilename=await saveFile(options.filename.replace(/\.html$/i,'.preview.png'),preview);
40
+ committed=true;
41
+ const data={success:true,format:'html',filename,previewFilename,html,engineVerified:true,packedGgbVerified:true,
42
+ layoutChecks:{loaded:true,equalAxes:layout.equalAxes,viewWidth:layout.viewWidth,viewHeight:layout.viewHeight},renderVerified:true,requiresNetwork:true,objectCount:checks.objects,
43
+ note:'Native execution, saved HTML reopen, packed GGB, equal axes and preview capture passed. Inspect the attached preview for visual quality.'};
44
+ return {content:[{type:'text' as const,text:JSON.stringify(data)},{type:'image' as const,mimeType:'image/png',data:Buffer.from(preview).toString('base64')}],structuredContent:data};
45
+ }finally{
46
+ if(!committed&&previewFilename)await unlink(previewFilename);
47
+ if(!committed&&filename)await unlink(filename);
48
+ await engine.cleanup();
49
+ }
50
+ }
@@ -0,0 +1,38 @@
1
+ import path from 'path';
2
+ import os from 'os';
3
+ import { existsSync, readFileSync } from 'fs';
4
+ import { mkdir, writeFile } from 'fs/promises';
5
+ import puppeteer from 'puppeteer-core';
6
+ const {PUPPETEER_REVISIONS}=require('puppeteer-core') as {PUPPETEER_REVISIONS:{chrome:string}};
7
+ import { Browser, computeExecutablePath, install } from '@puppeteer/browsers';
8
+ export const browserCache = () => path.join(process.env['GEOGEBRA_STUDIO_HOME'] || path.join(os.homedir(),'.cache','geogebra-studio'),'browsers');
9
+ export const managedBrowserPath = () => computeExecutablePath({cacheDir:browserCache(),browser:Browser.CHROME,buildId:PUPPETEER_REVISIONS.chrome});
10
+ export function browserCandidates(): string[] {
11
+ const explicit=process.env['PUPPETEER_EXECUTABLE_PATH'];
12
+ if(explicit)return [explicit];
13
+ return [
14
+ ...(process.platform==='darwin'?['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',path.join(os.homedir(),'Applications/Google Chrome.app/Contents/MacOS/Google Chrome')]:[]),
15
+ ...(process.platform==='win32'?[process.env['PROGRAMFILES'],process.env['PROGRAMFILES(X86)'],process.env['LOCALAPPDATA']].filter((v):v is string=>!!v).map(v=>path.join(v,'Google','Chrome','Application','chrome.exe')):[]),
16
+ ...(process.platform==='linux'?['/usr/bin/google-chrome','/usr/bin/google-chrome-stable','/usr/bin/chromium','/usr/bin/chromium-browser']:[]),
17
+ managedBrowserPath(),
18
+ ].filter((value,index,all)=>all.indexOf(value)===index);
19
+ }
20
+ /** Runtime lookup is cheap; installation/doctor additionally probes launch compatibility. */
21
+ export function resolveBrowser(): string {
22
+ if(!process.env['PUPPETEER_EXECUTABLE_PATH']){try{const selected=readFileSync(path.join(browserCache(),'selected.txt'),'utf8').trim();if(existsSync(selected))return selected;}catch{/* No prepared runtime yet. */}}
23
+ const executable=browserCandidates().find(file=>existsSync(file));
24
+ if(!executable)throw new Error('Chrome is unavailable. Run: npx @lydt/geogebra-studio browser install');
25
+ return executable;
26
+ }
27
+ async function rememberBrowser(file:string):Promise<string>{await mkdir(browserCache(),{recursive:true});await writeFile(path.join(browserCache(),'selected.txt'),file,'utf8');return file;}
28
+ export async function prepareBrowser(download=false): Promise<string> {
29
+ const failures:string[]=[];
30
+ for(const file of browserCandidates().filter(file=>existsSync(file))){
31
+ try{const browser=await puppeteer.launch({executablePath:file,headless:true,timeout:15000});await browser.close();return rememberBrowser(file);}
32
+ catch(error){failures.push(`${file}: ${error instanceof Error?error.message:String(error)}`);}
33
+ }
34
+ if(!download)throw new Error('No usable Chrome. Run: npx @lydt/geogebra-studio browser install. '+failures.join('; '));
35
+ const installed=await install({browser:Browser.CHROME,buildId:PUPPETEER_REVISIONS.chrome,cacheDir:browserCache()});
36
+ const browser=await puppeteer.launch({executablePath:installed.executablePath,headless:true,timeout:15000});await browser.close();
37
+ return rememberBrowser(installed.executablePath);
38
+ }
@@ -0,0 +1,43 @@
1
+ import path from 'path';
2
+ import os from 'os';
3
+ import { mkdir, writeFile, readFile, cp, access } from 'fs/promises';
4
+ import { spawn } from 'child_process';
5
+ import { prepareBrowser } from './browser';
6
+ export async function run(command:string,args:string[],cwd?:string):Promise<void>{
7
+ await new Promise<void>((resolve,reject)=>{
8
+ const child=spawn(command,args,{stdio:'inherit',shell:false,...(cwd?{cwd}: {})});
9
+ child.once('error',reject);child.once('close',code=>code===0?resolve():reject(new Error(`${command} exited with ${code}`)));
10
+ });
11
+ }
12
+ /** A persistent catalog uses an npm source, never the temporary npx package directory. */
13
+ export async function installPlugin(local=false):Promise<void>{
14
+ const metadata=JSON.parse(await readFile(path.join(__dirname,'../../package.json'),'utf8'));
15
+ const home=process.env['CODEX_HOME']||path.join(os.homedir(),'.codex');
16
+ const root=path.join(home,'geogebra-studio','marketplace');
17
+ await mkdir(path.join(root,'.agents','plugins'),{recursive:true});
18
+ let source:Record<string,string>={source:'npm',package:metadata.name,version:metadata.version};
19
+ if(local){
20
+ // Explicit authoring mode copies release files to a stable location and installs runtime deps.
21
+ const target=path.join(root,'plugin');
22
+ try{await access(path.join(target,'package.json'));throw new Error(`Local package already exists: ${target}. Use a fresh CODEX_HOME for a clean test.`);}catch(error){if((error as NodeJS.ErrnoException).code!=='ENOENT')throw error;}
23
+ await mkdir(target,{recursive:true});
24
+ const packageRoot=path.join(__dirname,'../..');
25
+ for(const entry of ['dist','runtime','gikz','skills','docs','plugin.json','mcp.json','.codex-plugin','.mcp.json','package.json','LICENSE','LICENSE-MIT','THIRD_PARTY_NOTICES.md'])await cp(path.join(packageRoot,entry),path.join(target,entry),{recursive:true});
26
+ const localPackagePath=path.join(target,'package.json');
27
+ const localPackage=JSON.parse(await readFile(localPackagePath,'utf8'));
28
+ delete localPackage.devDependencies;
29
+ await writeFile(localPackagePath,JSON.stringify(localPackage,null,2)+'\n');
30
+ await run(process.platform==='win32'?'npm.cmd':'npm',['install','--omit=dev','--ignore-scripts','--no-audit','--no-fund'],target);
31
+ // Authoring mode tests the CLI installed at this stable path. Keep its command
32
+ // portable and resolve the entry point from the plugin root.
33
+ const localServer={command:'node',args:['dist/cli.js','mcp'],cwd:'./',env:{GEOGEBRA_TOOL_PROFILE:'core',GEOGEBRA_PLUGIN_RUNTIME:'1'}};
34
+ await writeFile(path.join(target,'mcp.json'),JSON.stringify({$schema:'https://agent-plugins.org/schemas/1.0.0/mcp.schema.json',mcpServers:{geogebra:{type:'stdio',...localServer}}},null,2)+'\n');
35
+ await writeFile(path.join(target,'.mcp.json'),JSON.stringify({mcpServers:{geogebra:localServer}},null,2)+'\n');
36
+ source={source:'local',path:'./plugin'};
37
+ }
38
+ await writeFile(path.join(root,'.agents/plugins/marketplace.json'),JSON.stringify({name:'geogebra-studio',interface:{displayName:'GeoGebra Studio'},plugins:[{name:'geogebra-studio',source,policy:{installation:'AVAILABLE',authentication:'ON_INSTALL'},category:'Education'}]},null,2)+'\n');
39
+ await run('codex',['plugin','marketplace','add',root]);
40
+ await run('codex',['plugin','add','geogebra-studio@geogebra-studio']);
41
+ const executable=await prepareBrowser(true);
42
+ console.log(`GeoGebra Studio ${metadata.version} installed. Browser: ${executable}\nStart a new Codex session and invoke @geogebra-studio. Existing geogebra-geometry installations were not removed.`);
43
+ }
@@ -0,0 +1,96 @@
1
+ import { ToolDefinition } from '../types/mcp';
2
+ import { cleanWithGikz } from '../export/gikz';
3
+ import { instancePool } from './geogebra-tools';
4
+
5
+ const result = (value: Record<string, unknown>) => ({
6
+ content: [{ type: 'text' as const, text: JSON.stringify(value) }], structuredContent: value,
7
+ });
8
+
9
+ /** A schema-checked bridge to the public methods exposed by the loaded GeoGebra Apps API. */
10
+ export const appsApiTools: ToolDefinition[] = [
11
+ {
12
+ tool: {
13
+ name: 'geogebra_api_list_methods',
14
+ description: 'List callable methods exposed by the active GeoGebra Apps API. Availability depends on the loaded GeoGebra app/version.',
15
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
16
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
17
+ },
18
+ handler: async () => {
19
+ try {
20
+ const methods = await (await instancePool.getDefaultInstance()).listAppsApiMethods();
21
+ return result({ success: true, methods, count: methods.length });
22
+ } catch (error) {
23
+ return { ...result({ success: false, error: error instanceof Error ? error.message : String(error) }), isError: true };
24
+ }
25
+ },
26
+ },
27
+ {
28
+ tool: {
29
+ name: 'geogebra_api_call',
30
+ description: 'Call a public GeoGebra Apps API method by name with JSON arguments. Use geogebra_api_list_methods first. For one-shot callback exports set waitForCallbackIndex (0 for getBase64/exportSVG/getScreenshotBase64; 1 for exportPDF). For persistent listener functions put {"$ggbCallbackId":"my-id"} at that argument, then poll geogebra_api_get_callback_events; reuse the same marker to unregister. exportPGF always returns Gikz-cleaned standalone TikZ through this bridge. Destructive methods require allowDestructive=true.',
31
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
32
+ inputSchema: {
33
+ type: 'object',
34
+ properties: {
35
+ method: { type: 'string', minLength: 1, maxLength: 100 },
36
+ args: { type: 'array', maxItems: 20, items: {} },
37
+ waitForCallbackIndex: { type: 'integer', minimum: 0, maximum: 20 },
38
+ callbackTimeoutMs: { type: 'integer', minimum: 100, maximum: 60000 },
39
+ allowDestructive: { type: 'boolean', description: 'Required for methods that clear, replace, open, or remove a construction/applet.' },
40
+ },
41
+ required: ['method'], additionalProperties: false,
42
+ },
43
+ },
44
+ handler: async params => {
45
+ try {
46
+ const method = params['method'] as string;
47
+ if (method === 'exportPGF') {
48
+ const source = await (await instancePool.getDefaultInstance()).exportNativeTikZ();
49
+ const value = await cleanWithGikz(source, {standalone:true});
50
+ return result({success:true,method,value,backend:'native-pgf+gikz',cleaned:true,semantics:'visual-snapshot'});
51
+ }
52
+ const dangerous = new Set(['newConstruction','reset','openFile','setXML','evalXML','setBase64','remove']);
53
+ if (dangerous.has(method) && params['allowDestructive'] !== true) {
54
+ throw new Error(`${method} can replace or clear construction data; set allowDestructive=true to proceed`);
55
+ }
56
+ const args = (params['args'] as unknown[] | undefined) ?? [];
57
+ const callbacks: Array<{ argumentIndex: number; id: string }> = [];
58
+ for (let i = 0; i < args.length; i++) {
59
+ const arg = args[i];
60
+ if (arg && typeof arg === 'object' && !Array.isArray(arg) && typeof (arg as Record<string, unknown>)['$ggbCallbackId'] === 'string') {
61
+ callbacks.push({ argumentIndex: i, id: (arg as Record<string, unknown>)['$ggbCallbackId'] as string });
62
+ args[i] = null;
63
+ }
64
+ }
65
+ const value = await (await instancePool.getDefaultInstance()).callAppsApi(
66
+ method,
67
+ args,
68
+ {
69
+ ...(typeof params['waitForCallbackIndex'] === 'number' ? { waitForCallbackIndex: params['waitForCallbackIndex'] } : {}),
70
+ ...(typeof params['callbackTimeoutMs'] === 'number' ? { callbackTimeoutMs: params['callbackTimeoutMs'] } : {}),
71
+ ...(callbacks.length ? { callbacks } : {}),
72
+ },
73
+ );
74
+ return result({ success: true, method, value: value === undefined ? null : value });
75
+ } catch (error) {
76
+ return { ...result({ success: false, error: error instanceof Error ? error.message : String(error) }), isError: true };
77
+ }
78
+ },
79
+ },
80
+ {
81
+ tool: {
82
+ name: 'geogebra_api_get_callback_events',
83
+ description: 'Read events captured from persistent GeoGebra Apps API listeners registered through geogebra_api_call. Set clear=false to read without clearing the queue.',
84
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
85
+ inputSchema: { type: 'object', properties: { callbackId: { type: 'string', minLength: 1, maxLength: 80 }, clear: { type: 'boolean', default: true } }, required: ['callbackId'], additionalProperties: false },
86
+ },
87
+ handler: async params => {
88
+ try {
89
+ const events = await (await instancePool.getDefaultInstance()).readAppsApiCallbackEvents(params['callbackId'] as string, params['clear'] !== false);
90
+ return result({ success: true, callbackId: params['callbackId'], events });
91
+ } catch (error) {
92
+ return { ...result({ success: false, error: error instanceof Error ? error.message : String(error) }), isError: true };
93
+ }
94
+ },
95
+ },
96
+ ];