@mahe_pkm/buzl-html-editor 0.1.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,318 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ const fs = require('fs');
6
+ const http = require('http');
7
+ const net = require('net');
8
+ const path = require('path');
9
+ const readline = require('readline');
10
+ const { URL } = require('url');
11
+
12
+ const SITE_ROOT = path.resolve(process.env.BUZL_SITE_ROOT || process.cwd());
13
+ const DEFAULT_PORT = 3000;
14
+ const HOST = '127.0.0.1';
15
+ const MIN_NODE_MAJOR = 18;
16
+
17
+ const BLOCKED_SEGMENTS = new Set([
18
+ 'admin',
19
+ 'api',
20
+ 'node_modules',
21
+ '.git',
22
+ '.backups',
23
+ 'rollback_backups',
24
+ 'tests',
25
+ ]);
26
+
27
+ const BLOCKED_FILES = new Set([
28
+ '.env',
29
+ '.env_example',
30
+ '.env.example',
31
+ 'package.json',
32
+ 'package-lock.json',
33
+ 'server.js',
34
+ 'start-website.js',
35
+ 'start-website.cmd',
36
+ 'start-website.sh',
37
+ ]);
38
+
39
+ const BLOCKED_EXTENSIONS = new Set([
40
+ '.bat',
41
+ '.cmd',
42
+ '.log',
43
+ '.md',
44
+ '.sh',
45
+ ]);
46
+
47
+ const CONTENT_TYPES = {
48
+ '.avif': 'image/avif',
49
+ '.css': 'text/css; charset=utf-8',
50
+ '.csv': 'text/csv; charset=utf-8',
51
+ '.gif': 'image/gif',
52
+ '.htm': 'text/html; charset=utf-8',
53
+ '.html': 'text/html; charset=utf-8',
54
+ '.ico': 'image/x-icon',
55
+ '.jpeg': 'image/jpeg',
56
+ '.jpg': 'image/jpeg',
57
+ '.js': 'text/javascript; charset=utf-8',
58
+ '.json': 'application/json; charset=utf-8',
59
+ '.map': 'application/json; charset=utf-8',
60
+ '.mjs': 'text/javascript; charset=utf-8',
61
+ '.mp3': 'audio/mpeg',
62
+ '.mp4': 'video/mp4',
63
+ '.ogg': 'audio/ogg',
64
+ '.otf': 'font/otf',
65
+ '.pdf': 'application/pdf',
66
+ '.png': 'image/png',
67
+ '.svg': 'image/svg+xml',
68
+ '.txt': 'text/plain; charset=utf-8',
69
+ '.webm': 'video/webm',
70
+ '.webp': 'image/webp',
71
+ '.woff': 'font/woff',
72
+ '.woff2': 'font/woff2',
73
+ '.xml': 'application/xml; charset=utf-8',
74
+ };
75
+
76
+ function parsePort(value, fallback = DEFAULT_PORT) {
77
+ const text = String(value ?? '').trim();
78
+ if (!text) return fallback;
79
+
80
+ if (!/^\d+$/.test(text)) {
81
+ throw new Error('The port must contain numbers only.');
82
+ }
83
+
84
+ const port = Number(text);
85
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) {
86
+ throw new Error('Choose a port between 1024 and 65535.');
87
+ }
88
+
89
+ return port;
90
+ }
91
+
92
+ function askForPort() {
93
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
94
+ return Promise.resolve(DEFAULT_PORT);
95
+ }
96
+
97
+ const prompt = readline.createInterface({
98
+ input: process.stdin,
99
+ output: process.stdout,
100
+ });
101
+
102
+ return new Promise((resolve) => {
103
+ prompt.question(`Enter website port [${DEFAULT_PORT}]: `, (answer) => {
104
+ prompt.close();
105
+ resolve(parsePort(answer));
106
+ });
107
+ });
108
+ }
109
+
110
+ function ensureSupportedNode() {
111
+ const major = Number(process.versions.node.split('.')[0]);
112
+ if (!Number.isInteger(major) || major < MIN_NODE_MAJOR) {
113
+ throw new Error(
114
+ `Node.js ${MIN_NODE_MAJOR} or newer is required. Node.js 20 LTS is recommended.`,
115
+ );
116
+ }
117
+ }
118
+
119
+ function checkPort(port) {
120
+ return new Promise((resolve, reject) => {
121
+ const probe = net.createServer();
122
+ probe.unref();
123
+ probe.once('error', (error) => {
124
+ if (error.code === 'EADDRINUSE') {
125
+ reject(new Error(`Port ${port} is already in use. Choose another port.`));
126
+ } else {
127
+ reject(error);
128
+ }
129
+ });
130
+ probe.listen({ host: HOST, port }, () => probe.close(resolve));
131
+ });
132
+ }
133
+
134
+ function isInsidePath(parentDir, targetPath) {
135
+ const relative = path.relative(parentDir, targetPath);
136
+ return relative === ''
137
+ || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative));
138
+ }
139
+
140
+ function isBlockedPath(urlPath) {
141
+ const segments = urlPath
142
+ .replace(/\\/g, '/')
143
+ .split('/')
144
+ .filter(Boolean)
145
+ .map((segment) => segment.toLowerCase());
146
+
147
+ if (segments.some((segment) => segment.startsWith('.'))) return true;
148
+ if (segments.some((segment) => BLOCKED_SEGMENTS.has(segment))) return true;
149
+
150
+ const filename = segments.at(-1) || '';
151
+ if (BLOCKED_FILES.has(filename)) return true;
152
+ return BLOCKED_EXTENSIONS.has(path.extname(filename));
153
+ }
154
+
155
+ async function resolvePublicFile(siteRoot, pathname) {
156
+ let decoded;
157
+ try {
158
+ decoded = decodeURIComponent(pathname);
159
+ } catch {
160
+ return null;
161
+ }
162
+
163
+ if (decoded.includes('\0') || isBlockedPath(decoded)) return null;
164
+
165
+ const rootPath = path.resolve(siteRoot);
166
+ let candidate = path.resolve(rootPath, `.${decoded.replace(/\\/g, '/')}`);
167
+ if (!isInsidePath(rootPath, candidate)) return null;
168
+
169
+ let stats;
170
+ try {
171
+ stats = await fs.promises.stat(candidate);
172
+ } catch {
173
+ stats = null;
174
+ }
175
+
176
+ if (stats?.isDirectory()) {
177
+ candidate = path.join(candidate, 'index.html');
178
+ try {
179
+ stats = await fs.promises.stat(candidate);
180
+ } catch {
181
+ stats = null;
182
+ }
183
+ }
184
+
185
+ if (!stats && !path.extname(candidate)) {
186
+ const htmlCandidate = `${candidate}.html`;
187
+ try {
188
+ const htmlStats = await fs.promises.stat(htmlCandidate);
189
+ if (htmlStats.isFile()) {
190
+ candidate = htmlCandidate;
191
+ stats = htmlStats;
192
+ }
193
+ } catch {
194
+ stats = null;
195
+ }
196
+ }
197
+
198
+ if (!stats?.isFile()) return null;
199
+
200
+ let realRoot;
201
+ let realCandidate;
202
+ try {
203
+ [realRoot, realCandidate] = await Promise.all([
204
+ fs.promises.realpath(rootPath),
205
+ fs.promises.realpath(candidate),
206
+ ]);
207
+ } catch {
208
+ return null;
209
+ }
210
+
211
+ if (!isInsidePath(realRoot, realCandidate)) return null;
212
+ return { filePath: realCandidate, stats };
213
+ }
214
+
215
+ function sendText(res, statusCode, message) {
216
+ const body = Buffer.from(message, 'utf8');
217
+ res.writeHead(statusCode, {
218
+ 'Content-Type': 'text/plain; charset=utf-8',
219
+ 'Content-Length': body.length,
220
+ 'X-Content-Type-Options': 'nosniff',
221
+ });
222
+ res.end(body);
223
+ }
224
+
225
+ function createSiteServer({ siteRoot = SITE_ROOT } = {}) {
226
+ const resolvedRoot = path.resolve(siteRoot);
227
+
228
+ return http.createServer(async (req, res) => {
229
+ if (!['GET', 'HEAD'].includes(req.method || '')) {
230
+ res.setHeader('Allow', 'GET, HEAD');
231
+ sendText(res, 405, 'Method Not Allowed');
232
+ return;
233
+ }
234
+
235
+ let pathname;
236
+ try {
237
+ pathname = new URL(req.url || '/', 'http://localhost').pathname;
238
+ } catch {
239
+ sendText(res, 400, 'Bad Request');
240
+ return;
241
+ }
242
+
243
+ const resolved = await resolvePublicFile(resolvedRoot, pathname);
244
+ if (!resolved) {
245
+ sendText(res, 404, 'Page Not Found');
246
+ return;
247
+ }
248
+
249
+ const extension = path.extname(resolved.filePath).toLowerCase();
250
+ const contentType = CONTENT_TYPES[extension] || 'application/octet-stream';
251
+ const headers = {
252
+ 'Content-Type': contentType,
253
+ 'Content-Length': resolved.stats.size,
254
+ 'X-Content-Type-Options': 'nosniff',
255
+ 'Cache-Control': contentType.startsWith('text/html')
256
+ ? 'no-cache'
257
+ : 'public, max-age=300',
258
+ };
259
+
260
+ res.writeHead(200, headers);
261
+ if (req.method === 'HEAD') {
262
+ res.end();
263
+ return;
264
+ }
265
+
266
+ const stream = fs.createReadStream(resolved.filePath);
267
+ stream.once('error', () => {
268
+ if (!res.headersSent) sendText(res, 500, 'Could not read this file');
269
+ else res.destroy();
270
+ });
271
+ stream.pipe(res);
272
+ });
273
+ }
274
+
275
+ async function main() {
276
+ try {
277
+ ensureSupportedNode();
278
+ const port = process.argv[2]
279
+ ? parsePort(process.argv[2])
280
+ : await askForPort();
281
+ await checkPort(port);
282
+
283
+ if (!fs.existsSync(path.join(SITE_ROOT, 'index.html'))) {
284
+ console.warn('\nWarning: index.html was not found beside start-website.js.');
285
+ }
286
+
287
+ const server = createSiteServer();
288
+ server.once('error', (error) => {
289
+ console.error(`\nWebsite server error: ${error.message}\n`);
290
+ process.exitCode = 1;
291
+ });
292
+ server.listen(port, HOST, () => {
293
+ console.log('\nStarting public website only...');
294
+ console.log(`Website: http://localhost:${port}/`);
295
+ console.log('The editor and API are disabled in this mode.');
296
+ console.log('Press Ctrl+C to stop.\n');
297
+ });
298
+ } catch (error) {
299
+ console.error(`\nUnable to start: ${error.message}\n`);
300
+ process.exitCode = 1;
301
+ }
302
+ }
303
+
304
+ if (require.main === module) {
305
+ main();
306
+ }
307
+
308
+ module.exports = {
309
+ BLOCKED_FILES,
310
+ BLOCKED_SEGMENTS,
311
+ DEFAULT_PORT,
312
+ HOST,
313
+ checkPort,
314
+ createSiteServer,
315
+ isBlockedPath,
316
+ parsePort,
317
+ resolvePublicFile,
318
+ };
package/lib/cli.js ADDED
@@ -0,0 +1,267 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const net = require('net');
5
+ const path = require('path');
6
+ const readline = require('readline');
7
+ const { spawn } = require('child_process');
8
+
9
+ const LOCAL_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
10
+
11
+ function parsePort(value, fallback) {
12
+ const text = String(value ?? '').trim();
13
+ if (!text) return fallback;
14
+ if (!/^\d+$/.test(text)) throw new Error('The port must contain numbers only.');
15
+ const port = Number(text);
16
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) {
17
+ throw new Error('Choose a port between 1024 and 65535.');
18
+ }
19
+ return port;
20
+ }
21
+
22
+ function parseArgs(argv, defaults) {
23
+ const options = {
24
+ allowNetwork: false,
25
+ command: 'serve',
26
+ host: defaults.host,
27
+ hostProvided: false,
28
+ open: defaults.open,
29
+ openProvided: false,
30
+ port: undefined,
31
+ portProvided: false,
32
+ root: process.cwd(),
33
+ };
34
+
35
+ const args = [...argv];
36
+ if (['doctor', 'init'].includes(args[0])) options.command = args.shift();
37
+
38
+ for (let index = 0; index < args.length; index += 1) {
39
+ const arg = args[index];
40
+ if (arg === '--help' || arg === '-h') options.command = 'help';
41
+ else if (arg === '--version' || arg === '-v') options.command = 'version';
42
+ else if (arg === '--no-open') { options.open = false; options.openProvided = true; }
43
+ else if (arg === '--open') { options.open = true; options.openProvided = true; }
44
+ else if (arg === '--allow-network') options.allowNetwork = true;
45
+ else if (arg === '--root') options.root = requireValue(args, ++index, '--root');
46
+ else if (arg.startsWith('--root=')) options.root = arg.slice(7);
47
+ else if (arg === '--port' || arg === '-p') {
48
+ options.port = requireValue(args, ++index, arg);
49
+ options.portProvided = true;
50
+ } else if (arg.startsWith('--port=')) {
51
+ options.port = arg.slice(7);
52
+ options.portProvided = true;
53
+ } else if (arg === '--host') { options.host = requireValue(args, ++index, '--host'); options.hostProvided = true; }
54
+ else if (arg.startsWith('--host=')) { options.host = arg.slice(7); options.hostProvided = true; }
55
+ else if (/^\d+$/.test(arg) && !options.portProvided) {
56
+ options.port = arg;
57
+ options.portProvided = true;
58
+ } else {
59
+ throw new Error(`Unknown option: ${arg}`);
60
+ }
61
+ }
62
+
63
+ options.root = path.resolve(options.root);
64
+ return options;
65
+ }
66
+
67
+ function requireValue(args, index, option) {
68
+ const value = args[index];
69
+ if (!value || value.startsWith('--')) throw new Error(`${option} requires a value.`);
70
+ return value;
71
+ }
72
+
73
+ function loadConfig(siteRoot) {
74
+ const configPath = path.join(siteRoot, '.buzl', 'config.json');
75
+ if (!fs.existsSync(configPath)) return {};
76
+ try {
77
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
78
+ if (!config || Array.isArray(config) || typeof config !== 'object') {
79
+ throw new Error('the file must contain a JSON object');
80
+ }
81
+ return config;
82
+ } catch (error) {
83
+ throw new Error(`Could not read ${configPath}: ${error.message}`);
84
+ }
85
+ }
86
+
87
+ function resolveRuntimeOptions(options, defaults) {
88
+ const config = loadConfig(options.root);
89
+ const configuredPort = config[defaults.configPortKey] ?? config.port;
90
+ const portValue = options.portProvided
91
+ ? options.port
92
+ : process.env.PORT || configuredPort || defaults.port;
93
+ const host = options.hostProvided
94
+ ? options.host
95
+ : process.env.HOST || config.host || defaults.host;
96
+ const shouldOpen = options.openProvided
97
+ ? options.open
98
+ : typeof config.open === 'boolean' ? config.open : defaults.open;
99
+
100
+ if (!LOCAL_HOSTS.has(host) && !options.allowNetwork) {
101
+ throw new Error(
102
+ `Refusing network host ${host}. Add --allow-network only when LAN access is intentional.`,
103
+ );
104
+ }
105
+
106
+ return {
107
+ ...options,
108
+ config,
109
+ host,
110
+ open: shouldOpen,
111
+ port: parsePort(portValue, defaults.port),
112
+ portConfigured: options.portProvided || Boolean(process.env.PORT) || configuredPort != null,
113
+ };
114
+ }
115
+
116
+ function askForPort(label, defaultPort) {
117
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return Promise.resolve(defaultPort);
118
+ const prompt = readline.createInterface({ input: process.stdin, output: process.stdout });
119
+ return new Promise((resolve, reject) => {
120
+ prompt.question(`Enter ${label} port [${defaultPort}]: `, (answer) => {
121
+ prompt.close();
122
+ try {
123
+ resolve(parsePort(answer, defaultPort));
124
+ } catch (error) {
125
+ reject(error);
126
+ }
127
+ });
128
+ });
129
+ }
130
+
131
+ function checkPort(port, host) {
132
+ return new Promise((resolve, reject) => {
133
+ const probe = net.createServer();
134
+ probe.unref();
135
+ probe.once('error', (error) => {
136
+ if (error.code === 'EADDRINUSE') {
137
+ reject(new Error(`Port ${port} is already in use. Choose another port.`));
138
+ } else {
139
+ reject(error);
140
+ }
141
+ });
142
+ probe.listen({ host, port }, () => probe.close(resolve));
143
+ });
144
+ }
145
+
146
+ function ensureSiteRoot(siteRoot) {
147
+ if (!fs.existsSync(siteRoot)) throw new Error(`Website folder does not exist: ${siteRoot}`);
148
+ if (!fs.statSync(siteRoot).isDirectory()) throw new Error(`Website root is not a folder: ${siteRoot}`);
149
+ }
150
+
151
+ function listHtmlFiles(siteRoot) {
152
+ const skipped = new Set(['admin', 'node_modules', '.git', '.buzl', 'tests']);
153
+ const results = [];
154
+ function visit(directory, relative = '') {
155
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
156
+ if (entry.name.startsWith('.') || skipped.has(entry.name)) continue;
157
+ const childRelative = relative ? path.join(relative, entry.name) : entry.name;
158
+ const child = path.join(directory, entry.name);
159
+ if (entry.isDirectory()) visit(child, childRelative);
160
+ else if (entry.isFile() && entry.name.toLowerCase().endsWith('.html')) {
161
+ results.push(childRelative.replace(/\\/g, '/'));
162
+ }
163
+ }
164
+ }
165
+ visit(siteRoot);
166
+ return results.sort();
167
+ }
168
+
169
+ function runDoctor(siteRoot, port, host) {
170
+ ensureSiteRoot(siteRoot);
171
+ const pages = listHtmlFiles(siteRoot);
172
+ const checks = {
173
+ node: process.version,
174
+ root: siteRoot,
175
+ entryPage: fs.existsSync(path.join(siteRoot, 'index.html')),
176
+ htmlPages: pages.length,
177
+ assetsDirectory: fs.existsSync(path.join(siteRoot, 'assets')),
178
+ writable: canWrite(siteRoot),
179
+ anthropicConfigured: Boolean(process.env.ANTHROPIC_API_KEY),
180
+ openRouterConfigured: Boolean(process.env.OPENROUTER_API_KEY),
181
+ address: `http://${host}:${port}/`,
182
+ };
183
+ console.log('\nBuzl editor diagnostics');
184
+ console.table(checks);
185
+ if (!checks.entryPage) console.warn('Warning: index.html is not present in the website root.');
186
+ if (checks.htmlPages === 0) console.warn('Warning: no editable HTML pages were found.');
187
+ return checks;
188
+ }
189
+
190
+ function canWrite(directory) {
191
+ try {
192
+ fs.accessSync(directory, fs.constants.W_OK);
193
+ return true;
194
+ } catch {
195
+ return false;
196
+ }
197
+ }
198
+
199
+ function initializeSite(siteRoot) {
200
+ ensureSiteRoot(siteRoot);
201
+ const configDir = path.join(siteRoot, '.buzl');
202
+ const configPath = path.join(configDir, 'config.json');
203
+ const envExamplePath = path.join(siteRoot, '.env.example');
204
+ const gitignorePath = path.join(siteRoot, '.gitignore');
205
+ fs.mkdirSync(configDir, { recursive: true });
206
+
207
+ const created = [];
208
+ if (!fs.existsSync(configPath)) {
209
+ fs.writeFileSync(configPath, `${JSON.stringify({ editorPort: 4000, websitePort: 3000, host: '127.0.0.1', open: true }, null, 2)}\n`);
210
+ created.push(path.relative(siteRoot, configPath));
211
+ }
212
+ if (!fs.existsSync(envExamplePath)) {
213
+ fs.writeFileSync(envExamplePath, [
214
+ 'ANTHROPIC_API_KEY=',
215
+ 'OPENROUTER_API_KEY=',
216
+ '',
217
+ ].join('\n'));
218
+ created.push(path.relative(siteRoot, envExamplePath));
219
+ }
220
+
221
+ const ignoreRules = ['.env', '.buzl/generated/'];
222
+ const existingIgnore = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8') : '';
223
+ const missingRules = ignoreRules.filter((rule) => !existingIgnore.split(/\r?\n/).includes(rule));
224
+ if (missingRules.length) {
225
+ const prefix = existingIgnore && !existingIgnore.endsWith('\n') ? '\n' : '';
226
+ fs.appendFileSync(gitignorePath, `${prefix}# Buzl editor\n${missingRules.join('\n')}\n`);
227
+ created.push(path.relative(siteRoot, gitignorePath));
228
+ }
229
+
230
+ console.log(created.length ? `Created: ${created.join(', ')}` : 'Buzl configuration already exists.');
231
+ return created;
232
+ }
233
+
234
+ function openBrowser(url) {
235
+ let command;
236
+ let args;
237
+ if (process.platform === 'win32') {
238
+ command = 'cmd.exe';
239
+ args = ['/d', '/s', '/c', 'start', '', url];
240
+ } else if (process.platform === 'darwin') {
241
+ command = 'open';
242
+ args = [url];
243
+ } else {
244
+ command = 'xdg-open';
245
+ args = [url];
246
+ }
247
+ try {
248
+ const child = spawn(command, args, { detached: true, stdio: 'ignore' });
249
+ child.unref();
250
+ } catch {
251
+ // The printed URL remains available if no desktop browser opener exists.
252
+ }
253
+ }
254
+
255
+ module.exports = {
256
+ askForPort,
257
+ checkPort,
258
+ ensureSiteRoot,
259
+ initializeSite,
260
+ listHtmlFiles,
261
+ loadConfig,
262
+ openBrowser,
263
+ parseArgs,
264
+ parsePort,
265
+ resolveRuntimeOptions,
266
+ runDoctor,
267
+ };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@mahe_pkm/buzl-html-editor",
3
+ "version": "0.1.0",
4
+ "description": "Local visual editor and static-site server for multi-page HTML websites",
5
+ "author": "mahe_pkm",
6
+ "type": "commonjs",
7
+ "bin": {
8
+ "buzl-editor": "bin/buzl-editor.js",
9
+ "buzl-site": "bin/buzl-site.js"
10
+ },
11
+ "files": [
12
+ "bin/",
13
+ "dist/",
14
+ "lib/",
15
+ "templates/",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "build": "node scripts/build-package.js",
21
+ "prepack": "npm run build",
22
+ "test": "npm run build && node --test",
23
+ "verify:pack": "node scripts/verify-packed-install.js",
24
+ "pack:check": "npm pack --dry-run"
25
+ },
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "dependencies": {
30
+ "@anthropic-ai/sdk": "^0.124.0",
31
+ "cheerio": "^1.0.0-rc.12",
32
+ "dotenv": "^16.4.7",
33
+ "express": "^4.18.2",
34
+ "multer": "^2.1.1",
35
+ "sharp": "^0.35.4"
36
+ },
37
+ "overrides": {
38
+ "qs": "^6.16.0"
39
+ },
40
+ "keywords": [
41
+ "html",
42
+ "visual-editor",
43
+ "static-site",
44
+ "grapesjs",
45
+ "website-editor",
46
+ "avif"
47
+ ],
48
+ "license": "UNLICENSED",
49
+ "publishConfig": {
50
+ "access": "public"
51
+ }
52
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "editorPort": 4000,
3
+ "websitePort": 3000,
4
+ "host": "127.0.0.1",
5
+ "open": true
6
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "Website",
3
+ "businessCategory": "business",
4
+ "purpose": "website",
5
+ "tone_and_style": "clear and professional",
6
+ "context": ""
7
+ }