@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,78 @@
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
+ import type { ITool, ToolExecutionContext, ToolExecutionResult, JSONSchemaObject } from '@framers/agentos';
13
+ import type { GenerateWidgetInput, GenerateWidgetOutput } from '../types.js';
14
+ import type { WidgetWrapper } from '../WidgetWrapper.js';
15
+ import type { WidgetFileManager } from '../WidgetFileManager.js';
16
+ /**
17
+ * Generates self-contained interactive HTML/CSS/JS widgets.
18
+ *
19
+ * This tool accepts complete HTML content, wraps it with structural
20
+ * defaults and an error boundary via {@link WidgetWrapper}, saves the
21
+ * result to disk via {@link WidgetFileManager}, and returns the file
22
+ * path, URLs, and metadata needed to serve or embed the widget.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const tool = new GenerateWidgetTool(wrapper, fileManager);
27
+ * const result = await tool.execute({
28
+ * html: '<html><body><canvas id="c"></canvas><script>...</script></body></html>',
29
+ * title: '3D Solar System',
30
+ * description: 'Interactive Three.js solar system visualization',
31
+ * }, context);
32
+ * ```
33
+ */
34
+ export declare class GenerateWidgetTool implements ITool<GenerateWidgetInput, GenerateWidgetOutput> {
35
+ /** @inheritdoc */
36
+ readonly id = "widget-generator-v1";
37
+ /** @inheritdoc */
38
+ readonly name = "generate_widget";
39
+ /** @inheritdoc */
40
+ readonly displayName = "Generate Interactive Widget";
41
+ /** @inheritdoc */
42
+ readonly description: string;
43
+ /** @inheritdoc */
44
+ readonly category = "productivity";
45
+ /** @inheritdoc */
46
+ readonly version = "1.0.0";
47
+ /** @inheritdoc */
48
+ readonly hasSideEffects = true;
49
+ /** @inheritdoc */
50
+ readonly inputSchema: JSONSchemaObject;
51
+ /** @inheritdoc */
52
+ readonly requiredCapabilities: string[];
53
+ /** The safety wrapper that adds structural defaults and error boundary. */
54
+ private readonly wrapper;
55
+ /** The file manager responsible for persisting widgets to disk. */
56
+ private readonly fileManager;
57
+ /**
58
+ * Create a new GenerateWidgetTool instance.
59
+ *
60
+ * @param wrapper - The {@link WidgetWrapper} used to apply safety defaults.
61
+ * @param fileManager - The {@link WidgetFileManager} used for file persistence.
62
+ */
63
+ constructor(wrapper: WidgetWrapper, fileManager: WidgetFileManager);
64
+ /**
65
+ * Execute the widget generation pipeline.
66
+ *
67
+ * 1. Validates that the HTML contains at least one recognized HTML marker.
68
+ * 2. Applies the safety wrapper (doctype, charset, viewport, reset, error boundary).
69
+ * 3. Saves the wrapped HTML to disk with a timestamped, slugified filename.
70
+ * 4. Returns file path, URLs, inline eligibility, and the final HTML content.
71
+ *
72
+ * @param args - The {@link GenerateWidgetInput} containing HTML, title, and optional description.
73
+ * @param _context - The tool execution context (unused but required by the ITool interface).
74
+ * @returns A {@link ToolExecutionResult} wrapping the {@link GenerateWidgetOutput}.
75
+ */
76
+ execute(args: GenerateWidgetInput, _context: ToolExecutionContext): Promise<ToolExecutionResult<GenerateWidgetOutput>>;
77
+ }
78
+ //# sourceMappingURL=generateWidget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generateWidget.d.ts","sourceRoot":"","sources":["../../src/tools/generateWidget.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EACV,KAAK,EACL,oBAAoB,EACpB,mBAAmB,EACnB,gBAAgB,EACjB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,KAAK,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC7E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAcjE;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,kBAAmB,YAAW,KAAK,CAAC,mBAAmB,EAAE,oBAAoB,CAAC;IACzF,kBAAkB;IAClB,QAAQ,CAAC,EAAE,yBAAyB;IAEpC,kBAAkB;IAClB,QAAQ,CAAC,IAAI,qBAAqB;IAElC,kBAAkB;IAClB,QAAQ,CAAC,WAAW,iCAAiC;IAErD,kBAAkB;IAClB,QAAQ,CAAC,WAAW,SAG8B;IAElD,kBAAkB;IAClB,QAAQ,CAAC,QAAQ,kBAAkB;IAEnC,kBAAkB;IAClB,QAAQ,CAAC,OAAO,WAAW;IAE3B,kBAAkB;IAClB,QAAQ,CAAC,cAAc,QAAQ;IAE/B,kBAAkB;IAClB,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAiBpC;IAEF,kBAAkB;IAClB,QAAQ,CAAC,oBAAoB,WAAoC;IAEjE,2EAA2E;IAC3E,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IAExC,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoB;IAEhD;;;;;OAKG;gBACS,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,iBAAiB;IAKlE;;;;;;;;;;;OAWG;IACG,OAAO,CACX,IAAI,EAAE,mBAAmB,EACzB,QAAQ,EAAE,oBAAoB,GAC7B,OAAO,CAAC,mBAAmB,CAAC,oBAAoB,CAAC,CAAC;CA4CtD"}
@@ -0,0 +1,144 @@
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
+ * Inline size threshold in bytes. Widgets smaller than this are flagged
14
+ * as safe for inline embedding (e.g. in an iframe srcdoc attribute).
15
+ */
16
+ const INLINE_THRESHOLD_BYTES = 30 * 1024; // 30 KB
17
+ /**
18
+ * Minimum HTML markers that must appear in the input to be considered
19
+ * valid widget content. At least one must be present.
20
+ */
21
+ const VALID_HTML_MARKERS = ['<html', '<body', '<script', '<div', '<canvas', '<svg'];
22
+ /**
23
+ * Generates self-contained interactive HTML/CSS/JS widgets.
24
+ *
25
+ * This tool accepts complete HTML content, wraps it with structural
26
+ * defaults and an error boundary via {@link WidgetWrapper}, saves the
27
+ * result to disk via {@link WidgetFileManager}, and returns the file
28
+ * path, URLs, and metadata needed to serve or embed the widget.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const tool = new GenerateWidgetTool(wrapper, fileManager);
33
+ * const result = await tool.execute({
34
+ * html: '<html><body><canvas id="c"></canvas><script>...</script></body></html>',
35
+ * title: '3D Solar System',
36
+ * description: 'Interactive Three.js solar system visualization',
37
+ * }, context);
38
+ * ```
39
+ */
40
+ export class GenerateWidgetTool {
41
+ /** @inheritdoc */
42
+ id = 'widget-generator-v1';
43
+ /** @inheritdoc */
44
+ name = 'generate_widget';
45
+ /** @inheritdoc */
46
+ displayName = 'Generate Interactive Widget';
47
+ /** @inheritdoc */
48
+ description = 'Generate a self-contained interactive HTML/CSS/JS widget. ' +
49
+ 'Supports Three.js, D3.js, Chart.js, Plotly, Leaflet, p5.js, ' +
50
+ 'and any browser-based library loaded via CDN.';
51
+ /** @inheritdoc */
52
+ category = 'productivity';
53
+ /** @inheritdoc */
54
+ version = '1.0.0';
55
+ /** @inheritdoc */
56
+ hasSideEffects = true;
57
+ /** @inheritdoc */
58
+ inputSchema = {
59
+ type: 'object',
60
+ properties: {
61
+ html: {
62
+ type: 'string',
63
+ description: 'Complete HTML content for the widget.',
64
+ },
65
+ title: {
66
+ type: 'string',
67
+ description: 'Short title (used in filename and preview card).',
68
+ },
69
+ description: {
70
+ type: 'string',
71
+ description: 'Optional description shown in preview cards.',
72
+ },
73
+ },
74
+ required: ['html', 'title'],
75
+ };
76
+ /** @inheritdoc */
77
+ requiredCapabilities = ['capability:widget_generation'];
78
+ /** The safety wrapper that adds structural defaults and error boundary. */
79
+ wrapper;
80
+ /** The file manager responsible for persisting widgets to disk. */
81
+ fileManager;
82
+ /**
83
+ * Create a new GenerateWidgetTool instance.
84
+ *
85
+ * @param wrapper - The {@link WidgetWrapper} used to apply safety defaults.
86
+ * @param fileManager - The {@link WidgetFileManager} used for file persistence.
87
+ */
88
+ constructor(wrapper, fileManager) {
89
+ this.wrapper = wrapper;
90
+ this.fileManager = fileManager;
91
+ }
92
+ /**
93
+ * Execute the widget generation pipeline.
94
+ *
95
+ * 1. Validates that the HTML contains at least one recognized HTML marker.
96
+ * 2. Applies the safety wrapper (doctype, charset, viewport, reset, error boundary).
97
+ * 3. Saves the wrapped HTML to disk with a timestamped, slugified filename.
98
+ * 4. Returns file path, URLs, inline eligibility, and the final HTML content.
99
+ *
100
+ * @param args - The {@link GenerateWidgetInput} containing HTML, title, and optional description.
101
+ * @param _context - The tool execution context (unused but required by the ITool interface).
102
+ * @returns A {@link ToolExecutionResult} wrapping the {@link GenerateWidgetOutput}.
103
+ */
104
+ async execute(args, _context) {
105
+ // 1. Validate: check html contains at least one recognized marker
106
+ const lowerHtml = args.html.toLowerCase();
107
+ const hasValidMarker = VALID_HTML_MARKERS.some((marker) => lowerHtml.includes(marker));
108
+ if (!hasValidMarker) {
109
+ return {
110
+ success: false,
111
+ error: 'Invalid widget HTML: content must contain at least one of ' +
112
+ VALID_HTML_MARKERS.join(', ') +
113
+ '.',
114
+ };
115
+ }
116
+ try {
117
+ // 2. Wrap with safety defaults
118
+ const wrapped = this.wrapper.wrap(args.html);
119
+ // 3. Save to disk
120
+ const { filePath, filename } = await this.fileManager.save(wrapped, args.title);
121
+ // 4. Compute metadata
122
+ const sizeBytes = Buffer.byteLength(wrapped, 'utf-8');
123
+ const inline = sizeBytes < INLINE_THRESHOLD_BYTES;
124
+ return {
125
+ success: true,
126
+ output: {
127
+ filePath,
128
+ widgetUrl: this.fileManager.getWidgetUrl(filename),
129
+ downloadUrl: this.fileManager.getDownloadUrl(filename),
130
+ inline,
131
+ html: wrapped,
132
+ sizeBytes,
133
+ },
134
+ };
135
+ }
136
+ catch (err) {
137
+ return {
138
+ success: false,
139
+ error: `Widget generation failed: ${err.message}`,
140
+ };
141
+ }
142
+ }
143
+ }
144
+ //# sourceMappingURL=generateWidget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generateWidget.js","sourceRoot":"","sources":["../../src/tools/generateWidget.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
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
+ * Input to the `generate_widget` tool.
9
+ *
10
+ * The agent provides complete HTML content and a human-readable title.
11
+ * The extension handles safety wrapping, file persistence, and URL
12
+ * generation automatically.
13
+ */
14
+ export interface GenerateWidgetInput {
15
+ /** Complete HTML content for the widget. */
16
+ html: string;
17
+ /** Short title (used in filename and preview card). */
18
+ title: string;
19
+ /** Optional description shown in preview cards. */
20
+ description?: string;
21
+ }
22
+ /**
23
+ * Output from the `generate_widget` tool.
24
+ *
25
+ * Contains all the information needed to reference, embed, download,
26
+ * or inline the generated widget.
27
+ */
28
+ export interface GenerateWidgetOutput {
29
+ /** Absolute path to the saved HTML file. */
30
+ filePath: string;
31
+ /** HTTP URL to view the widget via the agent server. */
32
+ widgetUrl: string;
33
+ /** HTTP URL to download the raw HTML file. */
34
+ downloadUrl: string;
35
+ /** Whether the HTML is small enough to embed inline (<30 KB). */
36
+ inline: boolean;
37
+ /** The final HTML content with safety wrapper applied. */
38
+ html: string;
39
+ /** File size in bytes. */
40
+ sizeBytes: number;
41
+ }
42
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;IAEb,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAC;IAEd,mDAAmD;IACnD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,CAAC;IAEjB,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAC;IAElB,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IAEpB,iEAAiE;IACjE,MAAM,EAAE,OAAO,CAAC;IAEhB,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IAEb,0BAA0B;IAC1B,SAAS,EAAE,MAAM,CAAC;CACnB"}
package/dist/types.js ADDED
@@ -0,0 +1,8 @@
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
+ export {};
8
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
package/manifest.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "https://agentos.sh/schemas/extension-manifest-v1.json",
3
+ "id": "com.framers.productivity.widget-generator",
4
+ "name": "Interactive Widget Generator Extension",
5
+ "version": "1.0.0",
6
+ "description": "Generate self-contained interactive HTML/CSS/JS widgets with safety wrapping and file management",
7
+ "author": {
8
+ "name": "Frame.dev",
9
+ "email": "support@frame.dev",
10
+ "url": "https://github.com/framersai"
11
+ },
12
+ "license": "MIT",
13
+ "keywords": ["widget", "html", "interactive", "visualization", "productivity"],
14
+ "agentosVersion": "^2.0.0",
15
+ "categories": ["productivity", "visualization"],
16
+ "extensions": [
17
+ {
18
+ "kind": "tool",
19
+ "id": "generate_widget",
20
+ "displayName": "Generate Interactive Widget",
21
+ "description": "Generate a self-contained interactive HTML/CSS/JS widget",
22
+ "entry": "./dist/tools/generateWidget.js"
23
+ }
24
+ ],
25
+ "configuration": {}
26
+ }
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@framers/agentos-ext-widget-generator",
3
+ "version": "1.0.0",
4
+ "description": "Interactive HTML/CSS/JS widget generator tool for AgentOS",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ },
14
+ "./manifest.json": "./manifest.json",
15
+ "./package.json": "./package.json"
16
+ },
17
+ "keywords": [
18
+ "agentos",
19
+ "extension",
20
+ "widget",
21
+ "html",
22
+ "interactive",
23
+ "productivity"
24
+ ],
25
+ "author": {
26
+ "name": "Framers AI",
27
+ "email": "team@frame.dev",
28
+ "url": "https://frame.dev"
29
+ },
30
+ "contributors": [
31
+ {
32
+ "name": "Johnny Dunn",
33
+ "email": "johnnyfived@protonmail.com",
34
+ "url": "https://github.com/jddunn"
35
+ }
36
+ ],
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/framersai/agentos-extensions.git",
41
+ "directory": "registry/curated/productivity/widget-generator"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "peerDependencies": {
47
+ "@framers/agentos": "^0.1.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^20.12.12",
51
+ "rimraf": "^5.0.7",
52
+ "typescript": "^5.4.5",
53
+ "vitest": "^1.6.0",
54
+ "@framers/agentos": "0.1.126"
55
+ },
56
+ "scripts": {
57
+ "build": "tsc",
58
+ "test": "vitest run",
59
+ "test:watch": "vitest",
60
+ "test:coverage": "vitest run --coverage",
61
+ "lint": "eslint src --ext .ts",
62
+ "typecheck": "tsc --noEmit",
63
+ "clean": "rimraf dist"
64
+ }
65
+ }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * @module WidgetFileManager
3
+ *
4
+ * Manages the local widgets directory for generated HTML widgets. Provides
5
+ * methods to save widget HTML to disk, list existing widgets, delete files,
6
+ * resolve full paths, and construct view/download URLs.
7
+ *
8
+ * Files are stored under `{workspaceDir}/widgets/` with timestamped,
9
+ * slugified filenames to avoid collisions and ensure filesystem safety.
10
+ */
11
+
12
+ import { mkdir, writeFile, readdir, stat, unlink } from 'node:fs/promises';
13
+ import { existsSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+
16
+ /**
17
+ * Metadata returned when listing files in the widgets directory.
18
+ */
19
+ export interface WidgetFileEntry {
20
+ /** The file's base name (e.g. `2026-03-28T12-00-00-000Z-my-widget.html`). */
21
+ filename: string;
22
+
23
+ /** File size in bytes. */
24
+ sizeBytes: number;
25
+
26
+ /** ISO 8601 timestamp of when the file was created (from filesystem). */
27
+ createdAt: string;
28
+ }
29
+
30
+ /**
31
+ * Result returned after successfully saving a widget to disk.
32
+ */
33
+ export interface WidgetSaveResult {
34
+ /** Absolute path to the saved file. */
35
+ filePath: string;
36
+
37
+ /** The generated filename (basename only). */
38
+ filename: string;
39
+ }
40
+
41
+ /**
42
+ * Manages the widgets directory for generated HTML widgets. Handles saving,
43
+ * listing, deleting, and resolving paths for widget files.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * const manager = new WidgetFileManager('/home/agent/workspace', 3777);
48
+ * const { filePath, filename } = await manager.save('<html>...</html>', 'My Chart');
49
+ * const url = manager.getWidgetUrl(filename);
50
+ * ```
51
+ */
52
+ export class WidgetFileManager {
53
+ /** Absolute path to the widgets directory. */
54
+ private readonly widgetsDir: string;
55
+
56
+ /** Port used for constructing view and download URLs. */
57
+ private readonly serverPort: number;
58
+
59
+ /**
60
+ * Create a new WidgetFileManager instance.
61
+ *
62
+ * @param workspaceDir - Root workspace directory for the agent. The
63
+ * widgets directory will be created as a subdirectory named `widgets`.
64
+ * @param serverPort - Optional port number for constructing localhost
65
+ * view and download URLs. Defaults to `3777`.
66
+ */
67
+ constructor(workspaceDir: string, serverPort?: number) {
68
+ this.widgetsDir = join(workspaceDir, 'widgets');
69
+ this.serverPort = serverPort ?? 3777;
70
+ }
71
+
72
+ /**
73
+ * Save an HTML widget to the widgets directory.
74
+ *
75
+ * Creates the widgets directory if it does not already exist. The filename
76
+ * is generated from the current ISO timestamp and a slugified version of
77
+ * the widget title, ensuring uniqueness and filesystem safety.
78
+ *
79
+ * @param html - The complete HTML content to write.
80
+ * @param title - The widget title, used to derive the filename slug.
81
+ * @returns An object containing the absolute `filePath` and `filename`.
82
+ */
83
+ async save(html: string, title: string): Promise<WidgetSaveResult> {
84
+ if (!existsSync(this.widgetsDir)) {
85
+ await mkdir(this.widgetsDir, { recursive: true });
86
+ }
87
+
88
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
89
+ const slug = this.slugify(title);
90
+ const filename = `${timestamp}-${slug}.html`;
91
+ const filePath = join(this.widgetsDir, filename);
92
+
93
+ await writeFile(filePath, html, 'utf-8');
94
+
95
+ return { filePath, filename };
96
+ }
97
+
98
+ /**
99
+ * List all widget files in the widgets directory with their metadata.
100
+ *
101
+ * Returns an empty array if the widgets directory does not exist or
102
+ * contains no files. Non-file entries (directories, symlinks) are
103
+ * silently skipped.
104
+ *
105
+ * @returns An array of {@link WidgetFileEntry} objects sorted by filename
106
+ * (newest first due to the timestamp prefix convention).
107
+ */
108
+ async list(): Promise<WidgetFileEntry[]> {
109
+ if (!existsSync(this.widgetsDir)) {
110
+ return [];
111
+ }
112
+
113
+ const entries = await readdir(this.widgetsDir);
114
+ const results: WidgetFileEntry[] = [];
115
+
116
+ for (const entry of entries) {
117
+ const fullPath = join(this.widgetsDir, entry);
118
+
119
+ try {
120
+ const fileStat = await stat(fullPath);
121
+
122
+ if (!fileStat.isFile()) {
123
+ continue;
124
+ }
125
+
126
+ results.push({
127
+ filename: entry,
128
+ sizeBytes: fileStat.size,
129
+ createdAt: fileStat.birthtime.toISOString(),
130
+ });
131
+ } catch {
132
+ // Skip files that can't be stat'd (e.g. permission errors)
133
+ continue;
134
+ }
135
+ }
136
+
137
+ return results;
138
+ }
139
+
140
+ /**
141
+ * Remove a widget file from the widgets directory.
142
+ *
143
+ * @param filename - The basename of the file to delete.
144
+ * @returns `true` if the file was successfully deleted, `false` if it
145
+ * did not exist or could not be removed.
146
+ */
147
+ async remove(filename: string): Promise<boolean> {
148
+ const fullPath = join(this.widgetsDir, filename);
149
+
150
+ if (!existsSync(fullPath)) {
151
+ return false;
152
+ }
153
+
154
+ try {
155
+ await unlink(fullPath);
156
+ return true;
157
+ } catch {
158
+ return false;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Resolve a filename to its absolute path in the widgets directory.
164
+ *
165
+ * @param filename - The basename of the file to resolve.
166
+ * @returns The absolute file path if the file exists, or `null` if not found.
167
+ */
168
+ resolve(filename: string): string | null {
169
+ const fullPath = join(this.widgetsDir, filename);
170
+ return existsSync(fullPath) ? fullPath : null;
171
+ }
172
+
173
+ /**
174
+ * Construct a view URL for the given widget filename.
175
+ *
176
+ * Uses the configured server port to build a localhost URL. In
177
+ * production deployments the URL scheme would be replaced by a
178
+ * reverse-proxy or CDN URL.
179
+ *
180
+ * @param filename - The basename of the widget file.
181
+ * @returns A fully-qualified HTTP URL pointing to the widget.
182
+ */
183
+ getWidgetUrl(filename: string): string {
184
+ return `http://localhost:${this.serverPort}/widgets/${filename}`;
185
+ }
186
+
187
+ /**
188
+ * Construct a download URL for the given widget filename.
189
+ *
190
+ * Returns the same URL as {@link getWidgetUrl} since the widget is
191
+ * a self-contained HTML file that can be both viewed and downloaded.
192
+ *
193
+ * @param filename - The basename of the widget file.
194
+ * @returns A fully-qualified HTTP URL pointing to the file.
195
+ */
196
+ getDownloadUrl(filename: string): string {
197
+ return `http://localhost:${this.serverPort}/widgets/${filename}`;
198
+ }
199
+
200
+ /**
201
+ * Convert a title string into a URL- and filesystem-safe slug.
202
+ *
203
+ * Transforms the input to lowercase, replaces non-alphanumeric
204
+ * characters with hyphens, collapses consecutive hyphens, and trims
205
+ * leading/trailing hyphens.
206
+ *
207
+ * @param title - The raw title string to slugify.
208
+ * @returns A clean, lowercase slug suitable for filenames.
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * slugify('3D Solar System!') // '3d-solar-system'
213
+ * ```
214
+ */
215
+ private slugify(title: string): string {
216
+ return title
217
+ .toLowerCase()
218
+ .replace(/[^a-z0-9]+/g, '-')
219
+ .replace(/-+/g, '-')
220
+ .replace(/^-|-$/g, '');
221
+ }
222
+ }