@framers/agentos-ext-widget-generator 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,134 @@
1
+ /**
2
+ * @module WidgetWrapper
3
+ *
4
+ * Safety wrapper that ensures every generated widget has a well-formed
5
+ * HTML structure, sensible defaults, and a client-side error boundary.
6
+ *
7
+ * The wrapper is additive: it inspects the incoming HTML with
8
+ * case-insensitive matching and only injects elements that are missing.
9
+ * This prevents double-adding when the agent already includes a
10
+ * doctype, charset meta, viewport meta, or error handler.
11
+ */
12
+
13
+ /**
14
+ * Wraps raw HTML content with structural defaults and an error boundary.
15
+ *
16
+ * Guarantees that every widget has:
17
+ * - A `<!DOCTYPE html>` declaration
18
+ * - A `<meta charset="utf-8">` tag
19
+ * - A responsive viewport meta tag
20
+ * - A minimal CSS reset (margin/padding/box-sizing + system-ui font)
21
+ * - A `window.onerror` error boundary that surfaces runtime errors visually
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * const wrapper = new WidgetWrapper();
26
+ * const safe = wrapper.wrap('<div>Hello world</div>');
27
+ * // safe now contains a full HTML document with all defaults injected
28
+ * ```
29
+ */
30
+ export class WidgetWrapper {
31
+ /**
32
+ * Apply the safety wrapper to the given HTML string.
33
+ *
34
+ * Performs case-insensitive checks against the source HTML and only
35
+ * injects elements that are not already present. The injection order
36
+ * is: doctype, charset, viewport, CSS reset, error boundary.
37
+ *
38
+ * @param html - Raw HTML content to wrap.
39
+ * @returns The wrapped HTML with all missing defaults injected.
40
+ */
41
+ wrap(html: string): string {
42
+ const lower = html.toLowerCase();
43
+ let result = html;
44
+
45
+ // 1. Add <!DOCTYPE html> if missing
46
+ if (!lower.includes('<!doctype html>')) {
47
+ result = '<!DOCTYPE html>\n' + result;
48
+ }
49
+
50
+ // 2. Add <meta charset="utf-8"> if missing
51
+ if (!lower.includes('charset')) {
52
+ result = this.injectIntoHead(result, '<meta charset="utf-8">');
53
+ }
54
+
55
+ // 3. Add viewport meta if missing
56
+ if (!lower.includes('viewport')) {
57
+ result = this.injectIntoHead(
58
+ result,
59
+ '<meta name="viewport" content="width=device-width, initial-scale=1">',
60
+ );
61
+ }
62
+
63
+ // 4. Prepend CSS reset
64
+ if (!lower.includes('box-sizing: border-box')) {
65
+ const resetCss =
66
+ '<style>* { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: system-ui, sans-serif; }</style>';
67
+ result = this.injectIntoHead(result, resetCss);
68
+ }
69
+
70
+ // 5. Append error boundary script
71
+ if (!lower.includes('window.onerror')) {
72
+ const errorScript = `<script>
73
+ window.onerror = function(msg, url, line) {
74
+ var el = document.createElement('div');
75
+ el.style.cssText = 'position:fixed;top:0;left:0;right:0;padding:16px;background:#fee2e2;color:#991b1b;font:14px system-ui;z-index:99999';
76
+ el.textContent = 'Widget error: ' + msg + ' (line ' + line + ')';
77
+ document.body.prepend(el);
78
+ };
79
+ </script>`;
80
+ result = this.injectBeforeBodyClose(result, errorScript);
81
+ }
82
+
83
+ return result;
84
+ }
85
+
86
+ /**
87
+ * Inject content into the `<head>` section of the HTML document.
88
+ *
89
+ * If a `<head>` tag exists, the content is inserted right after it.
90
+ * Otherwise, it is prepended to the document (after the doctype, if
91
+ * present).
92
+ *
93
+ * @param html - The HTML document string.
94
+ * @param content - The HTML fragment to inject.
95
+ * @returns The modified HTML with the content injected.
96
+ */
97
+ private injectIntoHead(html: string, content: string): string {
98
+ const headIndex = html.toLowerCase().indexOf('<head>');
99
+
100
+ if (headIndex !== -1) {
101
+ const insertPos = headIndex + '<head>'.length;
102
+ return html.slice(0, insertPos) + '\n' + content + html.slice(insertPos);
103
+ }
104
+
105
+ // No <head> tag — insert after doctype if present, otherwise at the top
106
+ const doctypeEnd = html.toLowerCase().indexOf('<!doctype html>');
107
+ if (doctypeEnd !== -1) {
108
+ const insertPos = doctypeEnd + '<!doctype html>'.length;
109
+ return html.slice(0, insertPos) + '\n' + content + html.slice(insertPos);
110
+ }
111
+
112
+ return content + '\n' + html;
113
+ }
114
+
115
+ /**
116
+ * Inject content just before the closing `</body>` tag.
117
+ *
118
+ * If no `</body>` tag exists, the content is appended at the end
119
+ * of the document.
120
+ *
121
+ * @param html - The HTML document string.
122
+ * @param content - The HTML fragment to inject.
123
+ * @returns The modified HTML with the content injected.
124
+ */
125
+ private injectBeforeBodyClose(html: string, content: string): string {
126
+ const bodyCloseIndex = html.toLowerCase().indexOf('</body>');
127
+
128
+ if (bodyCloseIndex !== -1) {
129
+ return html.slice(0, bodyCloseIndex) + content + '\n' + html.slice(bodyCloseIndex);
130
+ }
131
+
132
+ return html + '\n' + content;
133
+ }
134
+ }
package/src/index.ts ADDED
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @module index
3
+ *
4
+ * Widget Generator Extension Pack — generates self-contained interactive
5
+ * HTML/CSS/JS widgets with safety wrapping and file management.
6
+ *
7
+ * Entry point for the extension; follows the standard AgentOS extension
8
+ * pack factory pattern (see {@link createExtensionPack}). The factory
9
+ * wires up the {@link WidgetWrapper} for HTML safety, the
10
+ * {@link WidgetFileManager} for file persistence, and registers the
11
+ * `generate_widget` tool.
12
+ */
13
+
14
+ import { WidgetWrapper } from './WidgetWrapper.js';
15
+ import { WidgetFileManager } from './WidgetFileManager.js';
16
+ import { GenerateWidgetTool } from './tools/generateWidget.js';
17
+
18
+ /**
19
+ * Options accepted by the Widget Generator extension pack factory.
20
+ */
21
+ export interface WidgetGeneratorExtensionOptions {
22
+ /** Override the default priority used when registering the tool. */
23
+ priority?: number;
24
+
25
+ /** Override the agent workspace directory (defaults to `process.cwd()`). */
26
+ workspaceDir?: string;
27
+
28
+ /** Override the server port used for widget URLs (defaults to `3777`). */
29
+ serverPort?: number;
30
+ }
31
+
32
+ /**
33
+ * Factory function called by the AgentOS extension loader. Returns a pack
34
+ * descriptor containing the `generate_widget` tool.
35
+ *
36
+ * The factory:
37
+ *
38
+ * 1. Resolves the workspace directory and server port from context options.
39
+ * 2. Creates the {@link WidgetWrapper} for HTML safety wrapping.
40
+ * 3. Creates the {@link WidgetFileManager} for file I/O.
41
+ * 4. Wires both into the {@link GenerateWidgetTool}.
42
+ * 5. Returns the tool as an extension pack descriptor with lifecycle hooks.
43
+ *
44
+ * @param context - Extension activation context provided by the AgentOS runtime.
45
+ * @returns An extension pack with tool descriptors and lifecycle hooks.
46
+ */
47
+ export function createExtensionPack(context: any) {
48
+ const options = (context.options || {}) as WidgetGeneratorExtensionOptions;
49
+
50
+ // Resolve configuration
51
+ const workspaceDir = options.workspaceDir ?? process.cwd();
52
+ const serverPort = options.serverPort ?? 3777;
53
+ const priority = options.priority ?? 50;
54
+
55
+ // Create dependencies
56
+ const wrapper = new WidgetWrapper();
57
+ const fileManager = new WidgetFileManager(workspaceDir, serverPort);
58
+
59
+ // Create the tool with all dependencies injected
60
+ const tool = new GenerateWidgetTool(wrapper, fileManager);
61
+
62
+ return {
63
+ name: '@framers/agentos-ext-widget-generator',
64
+ version: '1.0.0',
65
+ descriptors: [
66
+ {
67
+ // IMPORTANT: ToolExecutor uses descriptor id as the lookup key for tool calls.
68
+ // Keep it aligned with `tool.name`.
69
+ id: tool.name,
70
+ kind: 'tool' as const,
71
+ priority,
72
+ payload: tool,
73
+ },
74
+ ],
75
+ onActivate: async () =>
76
+ context.logger?.info('Interactive Widgets activated'),
77
+ onDeactivate: async () =>
78
+ context.logger?.info('Interactive Widgets deactivated'),
79
+ };
80
+ }
81
+
82
+ export default createExtensionPack;
83
+
84
+ // Re-export classes for direct consumption
85
+ export { WidgetWrapper } from './WidgetWrapper.js';
86
+ export { WidgetFileManager } from './WidgetFileManager.js';
87
+ export { GenerateWidgetTool } from './tools/generateWidget.js';
88
+
89
+ // Re-export all public types so consumers can `import { ... } from '@framers/agentos-ext-widget-generator'`.
90
+ export type { GenerateWidgetInput, GenerateWidgetOutput } from './types.js';
91
+ export type { WidgetFileEntry, WidgetSaveResult } from './WidgetFileManager.js';
@@ -0,0 +1,178 @@
1
+ /**
2
+ * @module generateWidget
3
+ *
4
+ * ITool implementation for the `generate_widget` tool. Accepts raw HTML
5
+ * content from an agent, applies the {@link WidgetWrapper} safety layer,
6
+ * persists the result via {@link WidgetFileManager}, and returns URLs
7
+ * and metadata for embedding or download.
8
+ *
9
+ * Supports any browser-based library loaded via CDN including Three.js,
10
+ * D3.js, Chart.js, Plotly, Leaflet, p5.js, and more.
11
+ */
12
+
13
+ import type {
14
+ ITool,
15
+ ToolExecutionContext,
16
+ ToolExecutionResult,
17
+ JSONSchemaObject,
18
+ } from '@framers/agentos';
19
+
20
+ import type { GenerateWidgetInput, GenerateWidgetOutput } from '../types.js';
21
+ import type { WidgetWrapper } from '../WidgetWrapper.js';
22
+ import type { WidgetFileManager } from '../WidgetFileManager.js';
23
+
24
+ /**
25
+ * Inline size threshold in bytes. Widgets smaller than this are flagged
26
+ * as safe for inline embedding (e.g. in an iframe srcdoc attribute).
27
+ */
28
+ const INLINE_THRESHOLD_BYTES = 30 * 1024; // 30 KB
29
+
30
+ /**
31
+ * Minimum HTML markers that must appear in the input to be considered
32
+ * valid widget content. At least one must be present.
33
+ */
34
+ const VALID_HTML_MARKERS = ['<html', '<body', '<script', '<div', '<canvas', '<svg'];
35
+
36
+ /**
37
+ * Generates self-contained interactive HTML/CSS/JS widgets.
38
+ *
39
+ * This tool accepts complete HTML content, wraps it with structural
40
+ * defaults and an error boundary via {@link WidgetWrapper}, saves the
41
+ * result to disk via {@link WidgetFileManager}, and returns the file
42
+ * path, URLs, and metadata needed to serve or embed the widget.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * const tool = new GenerateWidgetTool(wrapper, fileManager);
47
+ * const result = await tool.execute({
48
+ * html: '<html><body><canvas id="c"></canvas><script>...</script></body></html>',
49
+ * title: '3D Solar System',
50
+ * description: 'Interactive Three.js solar system visualization',
51
+ * }, context);
52
+ * ```
53
+ */
54
+ export class GenerateWidgetTool implements ITool<GenerateWidgetInput, GenerateWidgetOutput> {
55
+ /** @inheritdoc */
56
+ readonly id = 'widget-generator-v1';
57
+
58
+ /** @inheritdoc */
59
+ readonly name = 'generate_widget';
60
+
61
+ /** @inheritdoc */
62
+ readonly displayName = 'Generate Interactive Widget';
63
+
64
+ /** @inheritdoc */
65
+ readonly description =
66
+ 'Generate a self-contained interactive HTML/CSS/JS widget. ' +
67
+ 'Supports Three.js, D3.js, Chart.js, Plotly, Leaflet, p5.js, ' +
68
+ 'and any browser-based library loaded via CDN.';
69
+
70
+ /** @inheritdoc */
71
+ readonly category = 'productivity';
72
+
73
+ /** @inheritdoc */
74
+ readonly version = '1.0.0';
75
+
76
+ /** @inheritdoc */
77
+ readonly hasSideEffects = true;
78
+
79
+ /** @inheritdoc */
80
+ readonly inputSchema: JSONSchemaObject = {
81
+ type: 'object',
82
+ properties: {
83
+ html: {
84
+ type: 'string',
85
+ description: 'Complete HTML content for the widget.',
86
+ },
87
+ title: {
88
+ type: 'string',
89
+ description: 'Short title (used in filename and preview card).',
90
+ },
91
+ description: {
92
+ type: 'string',
93
+ description: 'Optional description shown in preview cards.',
94
+ },
95
+ },
96
+ required: ['html', 'title'],
97
+ };
98
+
99
+ /** @inheritdoc */
100
+ readonly requiredCapabilities = ['capability:widget_generation'];
101
+
102
+ /** The safety wrapper that adds structural defaults and error boundary. */
103
+ private readonly wrapper: WidgetWrapper;
104
+
105
+ /** The file manager responsible for persisting widgets to disk. */
106
+ private readonly fileManager: WidgetFileManager;
107
+
108
+ /**
109
+ * Create a new GenerateWidgetTool instance.
110
+ *
111
+ * @param wrapper - The {@link WidgetWrapper} used to apply safety defaults.
112
+ * @param fileManager - The {@link WidgetFileManager} used for file persistence.
113
+ */
114
+ constructor(wrapper: WidgetWrapper, fileManager: WidgetFileManager) {
115
+ this.wrapper = wrapper;
116
+ this.fileManager = fileManager;
117
+ }
118
+
119
+ /**
120
+ * Execute the widget generation pipeline.
121
+ *
122
+ * 1. Validates that the HTML contains at least one recognized HTML marker.
123
+ * 2. Applies the safety wrapper (doctype, charset, viewport, reset, error boundary).
124
+ * 3. Saves the wrapped HTML to disk with a timestamped, slugified filename.
125
+ * 4. Returns file path, URLs, inline eligibility, and the final HTML content.
126
+ *
127
+ * @param args - The {@link GenerateWidgetInput} containing HTML, title, and optional description.
128
+ * @param _context - The tool execution context (unused but required by the ITool interface).
129
+ * @returns A {@link ToolExecutionResult} wrapping the {@link GenerateWidgetOutput}.
130
+ */
131
+ async execute(
132
+ args: GenerateWidgetInput,
133
+ _context: ToolExecutionContext,
134
+ ): Promise<ToolExecutionResult<GenerateWidgetOutput>> {
135
+ // 1. Validate: check html contains at least one recognized marker
136
+ const lowerHtml = args.html.toLowerCase();
137
+ const hasValidMarker = VALID_HTML_MARKERS.some((marker) => lowerHtml.includes(marker));
138
+
139
+ if (!hasValidMarker) {
140
+ return {
141
+ success: false,
142
+ error:
143
+ 'Invalid widget HTML: content must contain at least one of ' +
144
+ VALID_HTML_MARKERS.join(', ') +
145
+ '.',
146
+ };
147
+ }
148
+
149
+ try {
150
+ // 2. Wrap with safety defaults
151
+ const wrapped = this.wrapper.wrap(args.html);
152
+
153
+ // 3. Save to disk
154
+ const { filePath, filename } = await this.fileManager.save(wrapped, args.title);
155
+
156
+ // 4. Compute metadata
157
+ const sizeBytes = Buffer.byteLength(wrapped, 'utf-8');
158
+ const inline = sizeBytes < INLINE_THRESHOLD_BYTES;
159
+
160
+ return {
161
+ success: true,
162
+ output: {
163
+ filePath,
164
+ widgetUrl: this.fileManager.getWidgetUrl(filename),
165
+ downloadUrl: this.fileManager.getDownloadUrl(filename),
166
+ inline,
167
+ html: wrapped,
168
+ sizeBytes,
169
+ },
170
+ };
171
+ } catch (err: any) {
172
+ return {
173
+ success: false,
174
+ error: `Widget generation failed: ${err.message}`,
175
+ };
176
+ }
177
+ }
178
+ }
package/src/types.ts ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @module types
3
+ *
4
+ * Shared type definitions for the Widget Generator extension. Defines the
5
+ * input and output shapes used by the {@link GenerateWidgetTool}.
6
+ */
7
+
8
+ /**
9
+ * Input to the `generate_widget` tool.
10
+ *
11
+ * The agent provides complete HTML content and a human-readable title.
12
+ * The extension handles safety wrapping, file persistence, and URL
13
+ * generation automatically.
14
+ */
15
+ export interface GenerateWidgetInput {
16
+ /** Complete HTML content for the widget. */
17
+ html: string;
18
+
19
+ /** Short title (used in filename and preview card). */
20
+ title: string;
21
+
22
+ /** Optional description shown in preview cards. */
23
+ description?: string;
24
+ }
25
+
26
+ /**
27
+ * Output from the `generate_widget` tool.
28
+ *
29
+ * Contains all the information needed to reference, embed, download,
30
+ * or inline the generated widget.
31
+ */
32
+ export interface GenerateWidgetOutput {
33
+ /** Absolute path to the saved HTML file. */
34
+ filePath: string;
35
+
36
+ /** HTTP URL to view the widget via the agent server. */
37
+ widgetUrl: string;
38
+
39
+ /** HTTP URL to download the raw HTML file. */
40
+ downloadUrl: string;
41
+
42
+ /** Whether the HTML is small enough to embed inline (<30 KB). */
43
+ inline: boolean;
44
+
45
+ /** The final HTML content with safety wrapper applied. */
46
+ html: string;
47
+
48
+ /** File size in bytes. */
49
+ sizeBytes: number;
50
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "lib": ["ES2022"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "noEmitOnError": false,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "resolveJsonModule": true,
17
+ "moduleResolution": "Bundler",
18
+ "types": ["node"]
19
+ },
20
+ "include": ["src/**/*"],
21
+ "exclude": ["node_modules", "dist", "test"]
22
+ }